📦 Turbo Golo
d710c1b added
.github/workflows/release.yml +137 -0 | new file mode 100644 | ||
| @@ -0,0 +1,137 @@ | ||
| 1 | +name: Release | |
| 2 | + | |
| 3 | +# Publishes a release with the staged binaries whenever a tag v* is pushed — | |
| 4 | +# what ./01-release.tag.sh does at its last line. They are built by | |
| 5 | +# ./02-build-releases.sh, the same script one runs on a laptop, so a local | |
| 6 | +# build and a published one are the same pipeline. | |
| 7 | +# | |
| 8 | +# The tag alone already publishes the module: `go install …@TAG` works the | |
| 9 | +# moment 01 has run, with or without this workflow. What this adds is the page | |
| 10 | +# a person reads, and one binary per platform with a checksum to verify it | |
| 11 | +# against — the thing somebody without a Go toolchain needs. | |
| 12 | +# | |
| 13 | +# Rickub runs this as an ordinary GitHub Actions workflow. Two platform facts | |
| 14 | +# matter here: the job's GITHUB_TOKEN is the ONLY credential the release API | |
| 15 | +# (the /gh shim behind $GITHUB_API_URL) accepts — a personal token is refused — | |
| 16 | +# and it is read-only unless the workflow asks for `contents: write` below. | |
| 17 | +# That is why there is no longer a token file to keep out of git, and no | |
| 18 | +# 02-release.publish.sh or 04-release.upload-binaries.sh to run by hand. | |
| 19 | +# | |
| 20 | +# No workflow_dispatch on purpose: Rickub's dispatch API fires EVERY | |
| 21 | +# dispatchable workflow of a ref, so a repository should declare at most one. | |
| 22 | +on: | |
| 23 | + push: | |
| 24 | + tags: | |
| 25 | + - "v*" | |
| 26 | + | |
| 27 | +permissions: | |
| 28 | + contents: write | |
| 29 | + | |
| 30 | +concurrency: | |
| 31 | + group: release-${{ github.ref_name }} | |
| 32 | + cancel-in-progress: false | |
| 33 | + | |
| 34 | +jobs: | |
| 35 | + release: | |
| 36 | + name: publish ${{ github.ref_name }} | |
| 37 | + runs-on: ubuntu-latest | |
| 38 | + steps: | |
| 39 | + - name: Checkout | |
| 40 | + uses: actions/checkout@v4 | |
| 41 | + with: | |
| 42 | + # The whole history and the tags: the release notes below are read | |
| 43 | + # from the annotated tag's message, and the Makefile's default | |
| 44 | + # version comes from `git describe`. | |
| 45 | + fetch-depth: 0 | |
| 46 | + | |
| 47 | + - name: Set up Go | |
| 48 | + uses: actions/setup-go@v5 | |
| 49 | + with: | |
| 50 | + go-version-file: go.mod | |
| 51 | + cache: true | |
| 52 | + | |
| 53 | + - name: go test | |
| 54 | + # The suite includes tests that run ./01-release.tag.sh against a | |
| 55 | + # throwaway clone. They skip themselves when they see this, exactly as | |
| 56 | + # they do when the script itself calls make check — without it, a | |
| 57 | + # release job would start a release inside itself. | |
| 58 | + env: | |
| 59 | + TURBO_GOLO_RELEASING: "1" | |
| 60 | + run: go test ./... -count=1 | |
| 61 | + | |
| 62 | + - name: Build the release | |
| 63 | + # release.env is git-ignored, so the tag is passed explicitly and the | |
| 64 | + # script falls back to "Turbo Golo <tag>" for the description. | |
| 65 | + run: bash ./02-build-releases.sh "${GITHUB_REF_NAME}" | |
| 66 | + | |
| 67 | + - name: Release notes | |
| 68 | + id: notes | |
| 69 | + # The message ./01-release.tag.sh put on the annotated tag (ABOUT in | |
| 70 | + # release.env), then the two ways to get the editor and the links to | |
| 71 | + # the documentation AT THAT TAG — a release page is not inside the | |
| 72 | + # repository tree, so a relative path from it 404s, and a link to the | |
| 73 | + # branch would rot as the branch moves. A lightweight tag has no | |
| 74 | + # message: the tag name stands in. | |
| 75 | + run: | | |
| 76 | + set -euo pipefail | |
| 77 | + message="$(git for-each-ref "refs/tags/${GITHUB_REF_NAME}" --format='%(contents)' | sed '/^-----BEGIN PGP SIGNATURE-----/,$d')" | |
| 78 | + if [ -z "$(printf '%s' "${message}" | tr -d '[:space:]')" ]; then | |
| 79 | + message="Turbo Golo ${GITHUB_REF_NAME}" | |
| 80 | + fi | |
| 81 | + tree="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${GITHUB_REF_NAME}" | |
| 82 | + version="${GITHUB_REF_NAME#v}" | |
| 83 | + { | |
| 84 | + printf '%s\n\n' "${message}" | |
| 85 | + echo "Download the binary for your platform below, or install from the module proxy:" | |
| 86 | + echo | |
| 87 | + echo '```bash' | |
| 88 | + echo "go install $(go list -m)@${GITHUB_REF_NAME}" | |
| 89 | + echo '```' | |
| 90 | + echo | |
| 91 | + echo "Documentation: [English](${tree}/docs/en/README.md) · [Français](${tree}/docs/fr/README.md) · [how to install](${tree}/docs/en/how-to/install.md)" | |
| 92 | + echo | |
| 93 | + echo "- Commit: \`${GITHUB_SHA}\`" | |
| 94 | + echo "- Published by the Release workflow, run #${GITHUB_RUN_NUMBER}, with $(go env GOVERSION)" | |
| 95 | + echo | |
| 96 | + echo '## Running a download' | |
| 97 | + echo | |
| 98 | + echo '```bash' | |
| 99 | + echo "chmod +x turbo-golo-${version}-<platform>" | |
| 100 | + echo "./turbo-golo-${version}-<platform> main.golo" | |
| 101 | + echo '```' | |
| 102 | + echo | |
| 103 | + echo "On macOS, an unsigned download is quarantined until you say otherwise: \`xattr -d com.apple.quarantine turbo-golo-${version}-darwin-arm64\`." | |
| 104 | + echo | |
| 105 | + echo '## Checksums' | |
| 106 | + echo | |
| 107 | + echo 'Verify a download with `sha256sum -c SHA256SUMS --ignore-missing` (`shasum -a 256 -c` on macOS).' | |
| 108 | + echo | |
| 109 | + echo '```' | |
| 110 | + cat "release/${GITHUB_REF_NAME}/SHA256SUMS" | |
| 111 | + echo '```' | |
| 112 | + } > "${RUNNER_TEMP}/notes.md" | |
| 113 | + echo "path=${RUNNER_TEMP}/notes.md" >> "$GITHUB_OUTPUT" | |
| 114 | + | |
| 115 | + - name: Keep the binaries as a run artifact | |
| 116 | + # Downloadable from the run page even if the publish step below fails | |
| 117 | + # (an old CI node that does not forward /gh answers 403 there). | |
| 118 | + uses: actions/upload-artifact@v4 | |
| 119 | + with: | |
| 120 | + name: turbo-golo-${{ github.ref_name }} | |
| 121 | + path: release/${{ github.ref_name }}/ | |
| 122 | + if-no-files-found: error | |
| 123 | + retention-days: 14 | |
| 124 | + | |
| 125 | + - name: Publish the release | |
| 126 | + uses: softprops/action-gh-release@v2 | |
| 127 | + with: | |
| 128 | + tag_name: ${{ github.ref_name }} | |
| 129 | + name: ${{ github.ref_name }} | |
| 130 | + body_path: ${{ steps.notes.outputs.path }} | |
| 131 | + draft: false | |
| 132 | + prerelease: ${{ contains(github.ref_name, '-') }} | |
| 133 | + files: | | |
| 134 | + release/${{ github.ref_name }}/turbo-golo-* | |
| 135 | + release/${{ github.ref_name }}/SHA256SUMS | |
| 136 | + release/${{ github.ref_name }}/README.md | |
| 137 | + fail_on_unmatched_files: true | |
| new file mode 100644 | |||
| @@ -0,0 +1,137 @@ | |||
| 1 | +name: Release | ||
| 2 | + | ||
| 3 | +# Publishes a release with the staged binaries whenever a tag v* is pushed — | ||
| 4 | +# what ./01-release.tag.sh does at its last line. They are built by | ||
| 5 | +# ./02-build-releases.sh, the same script one runs on a laptop, so a local | ||
| 6 | +# build and a published one are the same pipeline. | ||
| 7 | +# | ||
| 8 | +# The tag alone already publishes the module: `go install …@TAG` works the | ||
| 9 | +# moment 01 has run, with or without this workflow. What this adds is the page | ||
| 10 | +# a person reads, and one binary per platform with a checksum to verify it | ||
| 11 | +# against — the thing somebody without a Go toolchain needs. | ||
| 12 | +# | ||
| 13 | +# Rickub runs this as an ordinary GitHub Actions workflow. Two platform facts | ||
| 14 | +# matter here: the job's GITHUB_TOKEN is the ONLY credential the release API | ||
| 15 | +# (the /gh shim behind $GITHUB_API_URL) accepts — a personal token is refused — | ||
| 16 | +# and it is read-only unless the workflow asks for `contents: write` below. | ||
| 17 | +# That is why there is no longer a token file to keep out of git, and no | ||
| 18 | +# 02-release.publish.sh or 04-release.upload-binaries.sh to run by hand. | ||
| 19 | +# | ||
| 20 | +# No workflow_dispatch on purpose: Rickub's dispatch API fires EVERY | ||
| 21 | +# dispatchable workflow of a ref, so a repository should declare at most one. | ||
| 22 | +on: | ||
| 23 | + push: | ||
| 24 | + tags: | ||
| 25 | + - "v*" | ||
| 26 | + | ||
| 27 | +permissions: | ||
| 28 | + contents: write | ||
| 29 | + | ||
| 30 | +concurrency: | ||
| 31 | + group: release-${{ github.ref_name }} | ||
| 32 | + cancel-in-progress: false | ||
| 33 | + | ||
| 34 | +jobs: | ||
| 35 | + release: | ||
| 36 | + name: publish ${{ github.ref_name }} | ||
| 37 | + runs-on: ubuntu-latest | ||
| 38 | + steps: | ||
| 39 | + - name: Checkout | ||
| 40 | + uses: actions/checkout@v4 | ||
| 41 | + with: | ||
| 42 | + # The whole history and the tags: the release notes below are read | ||
| 43 | + # from the annotated tag's message, and the Makefile's default | ||
| 44 | + # version comes from `git describe`. | ||
| 45 | + fetch-depth: 0 | ||
| 46 | + | ||
| 47 | + - name: Set up Go | ||
| 48 | + uses: actions/setup-go@v5 | ||
| 49 | + with: | ||
| 50 | + go-version-file: go.mod | ||
| 51 | + cache: true | ||
| 52 | + | ||
| 53 | + - name: go test | ||
| 54 | + # The suite includes tests that run ./01-release.tag.sh against a | ||
| 55 | + # throwaway clone. They skip themselves when they see this, exactly as | ||
| 56 | + # they do when the script itself calls make check — without it, a | ||
| 57 | + # release job would start a release inside itself. | ||
| 58 | + env: | ||
| 59 | + TURBO_GOLO_RELEASING: "1" | ||
| 60 | + run: go test ./... -count=1 | ||
| 61 | + | ||
| 62 | + - name: Build the release | ||
| 63 | + # release.env is git-ignored, so the tag is passed explicitly and the | ||
| 64 | + # script falls back to "Turbo Golo <tag>" for the description. | ||
| 65 | + run: bash ./02-build-releases.sh "${GITHUB_REF_NAME}" | ||
| 66 | + | ||
| 67 | + - name: Release notes | ||
| 68 | + id: notes | ||
| 69 | + # The message ./01-release.tag.sh put on the annotated tag (ABOUT in | ||
| 70 | + # release.env), then the two ways to get the editor and the links to | ||
| 71 | + # the documentation AT THAT TAG — a release page is not inside the | ||
| 72 | + # repository tree, so a relative path from it 404s, and a link to the | ||
| 73 | + # branch would rot as the branch moves. A lightweight tag has no | ||
| 74 | + # message: the tag name stands in. | ||
| 75 | + run: | | ||
| 76 | + set -euo pipefail | ||
| 77 | + message="$(git for-each-ref "refs/tags/${GITHUB_REF_NAME}" --format='%(contents)' | sed '/^-----BEGIN PGP SIGNATURE-----/,$d')" | ||
| 78 | + if [ -z "$(printf '%s' "${message}" | tr -d '[:space:]')" ]; then | ||
| 79 | + message="Turbo Golo ${GITHUB_REF_NAME}" | ||
| 80 | + fi | ||
| 81 | + tree="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${GITHUB_REF_NAME}" | ||
| 82 | + version="${GITHUB_REF_NAME#v}" | ||
| 83 | + { | ||
| 84 | + printf '%s\n\n' "${message}" | ||
| 85 | + echo "Download the binary for your platform below, or install from the module proxy:" | ||
| 86 | + echo | ||
| 87 | + echo '```bash' | ||
| 88 | + echo "go install $(go list -m)@${GITHUB_REF_NAME}" | ||
| 89 | + echo '```' | ||
| 90 | + echo | ||
| 91 | + echo "Documentation: [English](${tree}/docs/en/README.md) · [Français](${tree}/docs/fr/README.md) · [how to install](${tree}/docs/en/how-to/install.md)" | ||
| 92 | + echo | ||
| 93 | + echo "- Commit: \`${GITHUB_SHA}\`" | ||
| 94 | + echo "- Published by the Release workflow, run #${GITHUB_RUN_NUMBER}, with $(go env GOVERSION)" | ||
| 95 | + echo | ||
| 96 | + echo '## Running a download' | ||
| 97 | + echo | ||
| 98 | + echo '```bash' | ||
| 99 | + echo "chmod +x turbo-golo-${version}-<platform>" | ||
| 100 | + echo "./turbo-golo-${version}-<platform> main.golo" | ||
| 101 | + echo '```' | ||
| 102 | + echo | ||
| 103 | + echo "On macOS, an unsigned download is quarantined until you say otherwise: \`xattr -d com.apple.quarantine turbo-golo-${version}-darwin-arm64\`." | ||
| 104 | + echo | ||
| 105 | + echo '## Checksums' | ||
| 106 | + echo | ||
| 107 | + echo 'Verify a download with `sha256sum -c SHA256SUMS --ignore-missing` (`shasum -a 256 -c` on macOS).' | ||
| 108 | + echo | ||
| 109 | + echo '```' | ||
| 110 | + cat "release/${GITHUB_REF_NAME}/SHA256SUMS" | ||
| 111 | + echo '```' | ||
| 112 | + } > "${RUNNER_TEMP}/notes.md" | ||
| 113 | + echo "path=${RUNNER_TEMP}/notes.md" >> "$GITHUB_OUTPUT" | ||
| 114 | + | ||
| 115 | + - name: Keep the binaries as a run artifact | ||
| 116 | + # Downloadable from the run page even if the publish step below fails | ||
| 117 | + # (an old CI node that does not forward /gh answers 403 there). | ||
| 118 | + uses: actions/upload-artifact@v4 | ||
| 119 | + with: | ||
| 120 | + name: turbo-golo-${{ github.ref_name }} | ||
| 121 | + path: release/${{ github.ref_name }}/ | ||
| 122 | + if-no-files-found: error | ||
| 123 | + retention-days: 14 | ||
| 124 | + | ||
| 125 | + - name: Publish the release | ||
| 126 | + uses: softprops/action-gh-release@v2 | ||
| 127 | + with: | ||
| 128 | + tag_name: ${{ github.ref_name }} | ||
| 129 | + name: ${{ github.ref_name }} | ||
| 130 | + body_path: ${{ steps.notes.outputs.path }} | ||
| 131 | + draft: false | ||
| 132 | + prerelease: ${{ contains(github.ref_name, '-') }} | ||
| 133 | + files: | | ||
| 134 | + release/${{ github.ref_name }}/turbo-golo-* | ||
| 135 | + release/${{ github.ref_name }}/SHA256SUMS | ||
| 136 | + release/${{ github.ref_name }}/README.md | ||
| 137 | + fail_on_unmatched_files: true | ||
added
.gitignore +10 -0 | new file mode 100644 | ||
| @@ -0,0 +1,10 @@ | ||
| 1 | +bin/ | |
| 2 | +kits | |
| 3 | +*.env | |
| 4 | +release | |
| 5 | + | |
| 6 | +# A go.work pointing at the checkout beside this one is how you build against an | |
| 7 | +# unreleased turbo-core. It is one person's local wiring, never the project's: | |
| 8 | +# committed, it would break every clone that has no such checkout. | |
| 9 | +go.work | |
| 10 | +go.work.sum | |
| new file mode 100644 | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | +bin/ | ||
| 2 | +kits | ||
| 3 | +*.env | ||
| 4 | +release | ||
| 5 | + | ||
| 6 | +# A go.work pointing at the checkout beside this one is how you build against an | ||
| 7 | +# unreleased turbo-core. It is one person's local wiring, never the project's: | ||
| 8 | +# committed, it would break every clone that has no such checkout. | ||
| 9 | +go.work | ||
| 10 | +go.work.sum | ||
added
.memory/README.md +11 -0 | new file mode 100644 | ||
| @@ -0,0 +1,11 @@ | ||
| 1 | +# .memory — the project record | |
| 2 | + | |
| 3 | +For whoever *continues building* Turbo Golo. `docs/` is for whoever *uses* it; the two are kept apart. | |
| 4 | + | |
| 5 | +| File | What it is | How it changes | | |
| 6 | +| --- | --- | --- | | |
| 7 | +| `summary.md` | A snapshot of the present: what this is, how to build and test it, the decisions in force, what is not yet established | Edited in place, only the parts a session verified; never regenerated | | |
| 8 | +| `history.md` | One dated entry per session: what was asked, what changed, why, what was rejected | Appended, never rewritten | | |
| 9 | +| `handoffs/YYYY-MM-DD-<slug>.md` | Where a session stopped: work in flight, next steps, traps | One per topic per day; never another session's | | |
| 10 | + | |
| 11 | +Read all three before doing anything; `summary.md` first, then the tail of `history.md`, then the newest handoff. | |
| new file mode 100644 | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | +# .memory — the project record | ||
| 2 | + | ||
| 3 | +For whoever *continues building* Turbo Golo. `docs/` is for whoever *uses* it; the two are kept apart. | ||
| 4 | + | ||
| 5 | +| File | What it is | How it changes | | ||
| 6 | +| --- | --- | --- | | ||
| 7 | +| `summary.md` | A snapshot of the present: what this is, how to build and test it, the decisions in force, what is not yet established | Edited in place, only the parts a session verified; never regenerated | | ||
| 8 | +| `history.md` | One dated entry per session: what was asked, what changed, why, what was rejected | Appended, never rewritten | | ||
| 9 | +| `handoffs/YYYY-MM-DD-<slug>.md` | Where a session stopped: work in flight, next steps, traps | One per topic per day; never another session's | | ||
| 10 | + | ||
| 11 | +Read all three before doing anything; `summary.md` first, then the tail of `history.md`, then the newest handoff. | ||
added
.memory/handoffs/2026-09-14-first-editor.md +36 -0 | new file mode 100644 | ||
| @@ -0,0 +1,36 @@ | ||
| 1 | +# Handoff — 2026-09-14 — Turbo Golo, first build | |
| 2 | + | |
| 3 | +## State | |
| 4 | + | |
| 5 | +The editor is complete and green. `make check` passes (250 tests, 0 skipped with `golo` on PATH), the quality gate passes, the documentation is complete in both languages, the root `README.md` is written, and the tutorial has been driven through a pty. Nothing is in flight. | |
| 6 | + | |
| 7 | +The working tree is **uncommitted**: everything sits untracked on `main` at `7069d2c`, the initial commit. No commit was asked for. | |
| 8 | + | |
| 9 | +## How this got here | |
| 10 | + | |
| 11 | +Two sessions on the same day. The first built everything and **ended before writing `.memory/`** — its only record was `/tmp/tg/FACTS.md` (its fact sheet for the docs), the pty dumps `/tmp/tg/*.raw`, and the drivers `/tmp/tg/pty_run.py` / `screen.py` / `wire.py`. The second session found the state from disk, wrote the missing French tutorial, the README and this memory, and registered the editor in the family. `/tmp` will not survive the sandbox; everything worth keeping from `FACTS.md` is now in `summary.md`. | |
| 12 | + | |
| 13 | +## What was touched outside this repository | |
| 14 | + | |
| 15 | +Three sibling repositories were edited, each with its own history entry and handoff, and each needs its own commit: | |
| 16 | + | |
| 17 | +- **turbo-core** — `README.md`, `docs/{en,fr}/README.md`, `docs/{en,fr}/how-to/test-without-publishing.md`, `profile/profile.go` (comment only), `.memory/summary.md`. Five editors now. Its `.go` strings were swept; **no code change**. | |
| 18 | +- **turbo-python** — `docs/{en,fr}/explanation/architecture.md`, "all four editors" → five. | |
| 19 | +- **turbo-moonbit** — the same sentence. **Its tree also carries the user's own uncommitted change** to `demos/hello/.turbo-moonbit/settings.toml`, which predates this work. | |
| 20 | + | |
| 21 | +## Next steps | |
| 22 | + | |
| 23 | +1. **Commit** — this repository, turbo-core, turbo-python, turbo-moonbit. | |
| 24 | +2. **Run a mutation pass over the tests before tagging.** No record says the first session did; every sibling's first session did. The scanner, the templates, the diagram test and the LSP tests are the four suites to break on purpose. `/tmp/falsify*.py` from turbo-moonbit's session show the in-process pattern (never a shell `cp` round trip — see that repository's handoff). | |
| 25 | +3. **Tag `v0.1.0`.** `release.env` already says so. `01-release.tag.sh` runs `make check` and refuses a live `replace`; it should go through as it stands. | |
| 26 | + | |
| 27 | +## Watch out for | |
| 28 | + | |
| 29 | +- **`golo` must be on PATH for the suite to mean anything.** The `golo lsp` tests skip themselves without it, and under `-short`. A green `go test -short ./...` proves nothing about the server. Here it is `/home/agent/bin/golo`, v0.1.1. | |
| 30 | +- **`golo` with no argument is the REPL**, reading stdin as Golo. The `lsp` in `ServerArgs()` is load-bearing; dropping it looks like a server that never answers and never dies. | |
| 31 | +- **`docs/*/reference/languages.md` is read by `reference_test.go`.** Reformatting that table breaks the test rather than the docs. | |
| 32 | +- **The builtins table is held to the real server's list.** A newer GoloScript that adds a builtin fails that test on purpose; update the table, do not loosen the test. | |
| 33 | +- **"Golo" is the language, "GoloScript" the implementation.** The docs keep the distinction everywhere; a mechanical edit that writes "GoloScript file" is wrong. | |
| 34 | +- **The Golo menu's `Run` asks which script to run** — there is no manifest, so nothing else could know. Six of the eight tools ask for something; that is the design, not an omission. | |
| 35 | +- **The user is on macOS; this sandbox is Linux.** `/tmp/tg/turbo-golo` is an ELF built for the pty run and lives outside the tree on purpose. Never leave a binary in `bin/`. | |
| 36 | +- **`cp` onto an existing file under `~/.claude/skills/` produced a file made entirely of NUL bytes, twice** — once over the old file and once after `rm`. Writing the bytes from Python (`open(dst,'wb').write(open(src,'rb').read())`) worked; verify with `cmp` afterwards. turbo-moonbit's handoff records the same filesystem doing this to Go sources during a `cp` round trip. Do not trust a `cp` on this sandbox without a `cmp`. | |
| new file mode 100644 | |||
| @@ -0,0 +1,36 @@ | |||
| 1 | +# Handoff — 2026-09-14 — Turbo Golo, first build | ||
| 2 | + | ||
| 3 | +## State | ||
| 4 | + | ||
| 5 | +The editor is complete and green. `make check` passes (250 tests, 0 skipped with `golo` on PATH), the quality gate passes, the documentation is complete in both languages, the root `README.md` is written, and the tutorial has been driven through a pty. Nothing is in flight. | ||
| 6 | + | ||
| 7 | +The working tree is **uncommitted**: everything sits untracked on `main` at `7069d2c`, the initial commit. No commit was asked for. | ||
| 8 | + | ||
| 9 | +## How this got here | ||
| 10 | + | ||
| 11 | +Two sessions on the same day. The first built everything and **ended before writing `.memory/`** — its only record was `/tmp/tg/FACTS.md` (its fact sheet for the docs), the pty dumps `/tmp/tg/*.raw`, and the drivers `/tmp/tg/pty_run.py` / `screen.py` / `wire.py`. The second session found the state from disk, wrote the missing French tutorial, the README and this memory, and registered the editor in the family. `/tmp` will not survive the sandbox; everything worth keeping from `FACTS.md` is now in `summary.md`. | ||
| 12 | + | ||
| 13 | +## What was touched outside this repository | ||
| 14 | + | ||
| 15 | +Three sibling repositories were edited, each with its own history entry and handoff, and each needs its own commit: | ||
| 16 | + | ||
| 17 | +- **turbo-core** — `README.md`, `docs/{en,fr}/README.md`, `docs/{en,fr}/how-to/test-without-publishing.md`, `profile/profile.go` (comment only), `.memory/summary.md`. Five editors now. Its `.go` strings were swept; **no code change**. | ||
| 18 | +- **turbo-python** — `docs/{en,fr}/explanation/architecture.md`, "all four editors" → five. | ||
| 19 | +- **turbo-moonbit** — the same sentence. **Its tree also carries the user's own uncommitted change** to `demos/hello/.turbo-moonbit/settings.toml`, which predates this work. | ||
| 20 | + | ||
| 21 | +## Next steps | ||
| 22 | + | ||
| 23 | +1. **Commit** — this repository, turbo-core, turbo-python, turbo-moonbit. | ||
| 24 | +2. **Run a mutation pass over the tests before tagging.** No record says the first session did; every sibling's first session did. The scanner, the templates, the diagram test and the LSP tests are the four suites to break on purpose. `/tmp/falsify*.py` from turbo-moonbit's session show the in-process pattern (never a shell `cp` round trip — see that repository's handoff). | ||
| 25 | +3. **Tag `v0.1.0`.** `release.env` already says so. `01-release.tag.sh` runs `make check` and refuses a live `replace`; it should go through as it stands. | ||
| 26 | + | ||
| 27 | +## Watch out for | ||
| 28 | + | ||
| 29 | +- **`golo` must be on PATH for the suite to mean anything.** The `golo lsp` tests skip themselves without it, and under `-short`. A green `go test -short ./...` proves nothing about the server. Here it is `/home/agent/bin/golo`, v0.1.1. | ||
| 30 | +- **`golo` with no argument is the REPL**, reading stdin as Golo. The `lsp` in `ServerArgs()` is load-bearing; dropping it looks like a server that never answers and never dies. | ||
| 31 | +- **`docs/*/reference/languages.md` is read by `reference_test.go`.** Reformatting that table breaks the test rather than the docs. | ||
| 32 | +- **The builtins table is held to the real server's list.** A newer GoloScript that adds a builtin fails that test on purpose; update the table, do not loosen the test. | ||
| 33 | +- **"Golo" is the language, "GoloScript" the implementation.** The docs keep the distinction everywhere; a mechanical edit that writes "GoloScript file" is wrong. | ||
| 34 | +- **The Golo menu's `Run` asks which script to run** — there is no manifest, so nothing else could know. Six of the eight tools ask for something; that is the design, not an omission. | ||
| 35 | +- **The user is on macOS; this sandbox is Linux.** `/tmp/tg/turbo-golo` is an ELF built for the pty run and lives outside the tree on purpose. Never leave a binary in `bin/`. | ||
| 36 | +- **`cp` onto an existing file under `~/.claude/skills/` produced a file made entirely of NUL bytes, twice** — once over the old file and once after `rm`. Writing the bytes from Python (`open(dst,'wb').write(open(src,'rb').read())`) worked; verify with `cmp` afterwards. turbo-moonbit's handoff records the same filesystem doing this to Go sources during a `cp` round trip. Do not trust a `cp` on this sandbox without a `cmp`. | ||
added
.memory/handoffs/2026-09-15-acp-commands-mentions.md +20 -0 | new file mode 100644 | ||
| @@ -0,0 +1,20 @@ | ||
| 1 | +# Handoff — 2026-09-15 — `/` commands and `@` mentions, documented | |
| 2 | + | |
| 3 | +## State | |
| 4 | + | |
| 5 | +Docs EN + FR and the starter template describe the `/` command list and the `@` file list that turbo-core's agent window gained on 2026-09-15. **Uncommitted.** Code unchanged here; suite green. | |
| 6 | + | |
| 7 | +## Next steps | |
| 8 | + | |
| 9 | +1. Wait for turbo-core to be tagged, then `go get codeberg.org/turbo-editors/turbo-core@vX.Y.Z`, `GOWORK=off make check`, tag. | |
| 10 | +2. Try `/` and `@` in a window by hand — nothing has been touched by a person. turbo-core's handoff of the same date has the list. | |
| 11 | + | |
| 12 | +## Watch out for | |
| 13 | + | |
| 14 | +- The six doc pages were edited by a script shared with four other editors, anchored on sentences that are identical across them. If you reword one of those sentences here, the next cross-editor edit will miss this repository — grep the other editors before rewording. | |
| 15 | + | |
| 16 | +## 2026-09-16 | |
| 17 | + | |
| 18 | +Added the `TURBO_ACP_TRACE` section and the "commands do not appear" bullet, EN + FR. The cause of the user's missing commands is still open — see turbo-core's handoff of 2026-09-15, "In flight". | |
| 19 | + | |
| 20 | +`demos/.turbo-golo/acp.toml` + `agent.yaml` were added the same day: Bob (docker agent) and mini-me (`mm -acp`). They load; nobody has opened them. Decide whether they are committed with the docs or kept local. | |
| new file mode 100644 | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | +# Handoff — 2026-09-15 — `/` commands and `@` mentions, documented | ||
| 2 | + | ||
| 3 | +## State | ||
| 4 | + | ||
| 5 | +Docs EN + FR and the starter template describe the `/` command list and the `@` file list that turbo-core's agent window gained on 2026-09-15. **Uncommitted.** Code unchanged here; suite green. | ||
| 6 | + | ||
| 7 | +## Next steps | ||
| 8 | + | ||
| 9 | +1. Wait for turbo-core to be tagged, then `go get codeberg.org/turbo-editors/turbo-core@vX.Y.Z`, `GOWORK=off make check`, tag. | ||
| 10 | +2. Try `/` and `@` in a window by hand — nothing has been touched by a person. turbo-core's handoff of the same date has the list. | ||
| 11 | + | ||
| 12 | +## Watch out for | ||
| 13 | + | ||
| 14 | +- The six doc pages were edited by a script shared with four other editors, anchored on sentences that are identical across them. If you reword one of those sentences here, the next cross-editor edit will miss this repository — grep the other editors before rewording. | ||
| 15 | + | ||
| 16 | +## 2026-09-16 | ||
| 17 | + | ||
| 18 | +Added the `TURBO_ACP_TRACE` section and the "commands do not appear" bullet, EN + FR. The cause of the user's missing commands is still open — see turbo-core's handoff of 2026-09-15, "In flight". | ||
| 19 | + | ||
| 20 | +`demos/.turbo-golo/acp.toml` + `agent.yaml` were added the same day: Bob (docker agent) and mini-me (`mm -acp`). They load; nobody has opened them. Decide whether they are committed with the docs or kept local. | ||
added
.memory/handoffs/2026-09-15-acp.md +29 -0 | new file mode 100644 | ||
| @@ -0,0 +1,29 @@ | ||
| 1 | +# Handoff — 2026-09-15 — ACP agent windows, ported | |
| 2 | + | |
| 3 | +## State | |
| 4 | + | |
| 5 | +**Done.** Branch `feature/acp`, local, **not committed and not pushed**. | |
| 6 | + | |
| 7 | +The feature is turbo-core's; this repository contributes `internal/*/acp.toml.tmpl` and one line in the profile. Tests green, quality gate PASS, documentation EN + FR. | |
| 8 | + | |
| 9 | +Read **turbo-core's** handoff of the same date first: it holds the design, and every trap worth knowing. | |
| 10 | + | |
| 11 | +## In flight | |
| 12 | + | |
| 13 | +Nothing. | |
| 14 | + | |
| 15 | +## Next steps | |
| 16 | + | |
| 17 | +1. **turbo-core must be released first.** This repository cannot build against an unreleased library from a clean clone. Order: tag and release turbo-core → `go get codeberg.org/turbo-editors/turbo-core@vX.Y.Z` here → **`GOWORK=off make check`** → tag and release this. | |
| 18 | +2. Open the editor, press `Alt-A`, and talk to an agent. Nobody has done that from **this** editor — only from turbo-go. | |
| 19 | + | |
| 20 | +## Watch out for | |
| 21 | + | |
| 22 | +- **`go.work` is what makes this build against the local turbo-core**, and it is gitignored. `GOWORK=off` is the only way to see what a clean clone sees. | |
| 23 | +- **The sandbox's filesystem corrupts `cp` for some recently-written inodes** — a file reads correctly through `cat`, `git` and `go build`, and comes out of `cp` as the right number of NUL bytes. It bit this port twice. Never use `cp` or a cross-filesystem `mv` to back a file up here; use `cat`. | |
| 24 | +- **One sentence in the starter file is about this editor and not the protocol** — the ```golo fence and the file extension beside it. `TestTheCreatedAgentsFileNamesThisEditorsOwnLanguage` is what stops a future copy-paste leaving another editor's language in it. | |
| 25 | +- **The documentation was adapted, not copied.** The slug, the language name, the fence, the build command and the language server were all replaced. If you add a page, do the same rather than translating turbo-go's wholesale. | |
| 26 | + | |
| 27 | +## Never touched by a human, here | |
| 28 | + | |
| 29 | +Everything. The agent window has only ever been opened from turbo-go. | |
| new file mode 100644 | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | +# Handoff — 2026-09-15 — ACP agent windows, ported | ||
| 2 | + | ||
| 3 | +## State | ||
| 4 | + | ||
| 5 | +**Done.** Branch `feature/acp`, local, **not committed and not pushed**. | ||
| 6 | + | ||
| 7 | +The feature is turbo-core's; this repository contributes `internal/*/acp.toml.tmpl` and one line in the profile. Tests green, quality gate PASS, documentation EN + FR. | ||
| 8 | + | ||
| 9 | +Read **turbo-core's** handoff of the same date first: it holds the design, and every trap worth knowing. | ||
| 10 | + | ||
| 11 | +## In flight | ||
| 12 | + | ||
| 13 | +Nothing. | ||
| 14 | + | ||
| 15 | +## Next steps | ||
| 16 | + | ||
| 17 | +1. **turbo-core must be released first.** This repository cannot build against an unreleased library from a clean clone. Order: tag and release turbo-core → `go get codeberg.org/turbo-editors/turbo-core@vX.Y.Z` here → **`GOWORK=off make check`** → tag and release this. | ||
| 18 | +2. Open the editor, press `Alt-A`, and talk to an agent. Nobody has done that from **this** editor — only from turbo-go. | ||
| 19 | + | ||
| 20 | +## Watch out for | ||
| 21 | + | ||
| 22 | +- **`go.work` is what makes this build against the local turbo-core**, and it is gitignored. `GOWORK=off` is the only way to see what a clean clone sees. | ||
| 23 | +- **The sandbox's filesystem corrupts `cp` for some recently-written inodes** — a file reads correctly through `cat`, `git` and `go build`, and comes out of `cp` as the right number of NUL bytes. It bit this port twice. Never use `cp` or a cross-filesystem `mv` to back a file up here; use `cat`. | ||
| 24 | +- **One sentence in the starter file is about this editor and not the protocol** — the ```golo fence and the file extension beside it. `TestTheCreatedAgentsFileNamesThisEditorsOwnLanguage` is what stops a future copy-paste leaving another editor's language in it. | ||
| 25 | +- **The documentation was adapted, not copied.** The slug, the language name, the fence, the build command and the language server were all replaced. If you add a page, do the same rather than translating turbo-go's wholesale. | ||
| 26 | + | ||
| 27 | +## Never touched by a human, here | ||
| 28 | + | ||
| 29 | +Everything. The agent window has only ever been opened from turbo-go. | ||
added
.memory/handoffs/2026-09-16-family-count.md +9 -0 | new file mode 100644 | ||
| @@ -0,0 +1,9 @@ | ||
| 1 | +# Handoff — 2026-09-16 — family count | |
| 2 | + | |
| 3 | +## State | |
| 4 | + | |
| 5 | +One-word edits in 3 documentation page(s) so that the family is counted at six editors (Turbo Go, Rust, Python, MoonBit, Golo, JS). Uncommitted. Nothing in flight. | |
| 6 | + | |
| 7 | +## Next steps | |
| 8 | + | |
| 9 | +1. Commit with whatever else is pending here. No release needed: the pages describe the family, not this editor's behaviour. | |
| new file mode 100644 | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | +# Handoff — 2026-09-16 — family count | ||
| 2 | + | ||
| 3 | +## State | ||
| 4 | + | ||
| 5 | +One-word edits in 3 documentation page(s) so that the family is counted at six editors (Turbo Go, Rust, Python, MoonBit, Golo, JS). Uncommitted. Nothing in flight. | ||
| 6 | + | ||
| 7 | +## Next steps | ||
| 8 | + | ||
| 9 | +1. Commit with whatever else is pending here. No release needed: the pages describe the family, not this editor's behaviour. | ||
added
.memory/handoffs/2026-09-17-windows-terminal-docs.md +15 -0 | new file mode 100644 | ||
| @@ -0,0 +1,15 @@ | ||
| 1 | +# Handoff — 2026-09-17 — Windows terminal windows: documentation ahead of the binary | |
| 2 | + | |
| 3 | +## State | |
| 4 | + | |
| 5 | +Six documentation pages per language and the README now say terminal windows and the tools menu work on Windows (pseudo-console, cmd.exe via `%COMSPEC%`). The binary built from this checkout still pins turbo-core **v0.8.0**, which has neither. Uncommitted. Nothing else in flight. | |
| 6 | + | |
| 7 | +## Next steps | |
| 8 | + | |
| 9 | +1. Wait for turbo-core `v0.9.0` (the Windows work sits uncommitted on turbo-core's `main` — see its `handoffs/2026-09-17-windows-terminal.md`). | |
| 10 | +2. `go get codeberg.org/turbo-editors/turbo-core@v0.9.0 && go mod tidy && GOWORK=off make check`, then tag. `tools.Shell` became `tools.Shell()`; nothing in this repository calls it, so the re-pin should be one line. | |
| 11 | +3. The first `F8` on a Windows machine, by whoever has one: the five checks are in `docs/*/how-to/use-a-terminal.md`. | |
| 12 | + | |
| 13 | +## Watch out for | |
| 14 | + | |
| 15 | +- The docs claim Windows support that **has never been run by the authors**, and say so in every place they claim it. Do not soften the wording until somebody has pressed `F8` on Windows. | |
| new file mode 100644 | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | +# Handoff — 2026-09-17 — Windows terminal windows: documentation ahead of the binary | ||
| 2 | + | ||
| 3 | +## State | ||
| 4 | + | ||
| 5 | +Six documentation pages per language and the README now say terminal windows and the tools menu work on Windows (pseudo-console, cmd.exe via `%COMSPEC%`). The binary built from this checkout still pins turbo-core **v0.8.0**, which has neither. Uncommitted. Nothing else in flight. | ||
| 6 | + | ||
| 7 | +## Next steps | ||
| 8 | + | ||
| 9 | +1. Wait for turbo-core `v0.9.0` (the Windows work sits uncommitted on turbo-core's `main` — see its `handoffs/2026-09-17-windows-terminal.md`). | ||
| 10 | +2. `go get codeberg.org/turbo-editors/turbo-core@v0.9.0 && go mod tidy && GOWORK=off make check`, then tag. `tools.Shell` became `tools.Shell()`; nothing in this repository calls it, so the re-pin should be one line. | ||
| 11 | +3. The first `F8` on a Windows machine, by whoever has one: the five checks are in `docs/*/how-to/use-a-terminal.md`. | ||
| 12 | + | ||
| 13 | +## Watch out for | ||
| 14 | + | ||
| 15 | +- The docs claim Windows support that **has never been run by the authors**, and say so in every place they claim it. Do not soften the wording until somebody has pressed `F8` on Windows. | ||
added
.memory/handoffs/2026-09-18-untitled-lsp-docs.md +13 -0 | new file mode 100644 | ||
| @@ -0,0 +1,13 @@ | ||
| 1 | +# Handoff — 2026-09-18 — Untitled-window LSP fix: docs added, re-pin pending | |
| 2 | + | |
| 3 | +## State | |
| 4 | + | |
| 5 | +turbo-core fixed the "window that started Untitled has no LSP until a restart" defect (its `.memory/handoffs/2026-09-18-first-launch-lsp.md` has the whole story). Here, only `docs/{en,fr}/how-to/enable-completion.md` gained the matching variant, inserted right after the `-no-lsp` block. Not committed. | |
| 6 | + | |
| 7 | +## Next steps | |
| 8 | + | |
| 9 | +1. When turbo-core is tagged and released: bump the `require` in `go.mod`, `go mod tidy`, rebuild, and drive it once — type into an Untitled window, save it under the language's extension, ask for a completion. | |
| 10 | + | |
| 11 | +## Watch out for | |
| 12 | + | |
| 13 | +- **The docs are ahead of the binary until that re-pin**: an editor built from the current pin still has the defect the new variant says is gone. | |
| new file mode 100644 | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | +# Handoff — 2026-09-18 — Untitled-window LSP fix: docs added, re-pin pending | ||
| 2 | + | ||
| 3 | +## State | ||
| 4 | + | ||
| 5 | +turbo-core fixed the "window that started Untitled has no LSP until a restart" defect (its `.memory/handoffs/2026-09-18-first-launch-lsp.md` has the whole story). Here, only `docs/{en,fr}/how-to/enable-completion.md` gained the matching variant, inserted right after the `-no-lsp` block. Not committed. | ||
| 6 | + | ||
| 7 | +## Next steps | ||
| 8 | + | ||
| 9 | +1. When turbo-core is tagged and released: bump the `require` in `go.mod`, `go mod tidy`, rebuild, and drive it once — type into an Untitled window, save it under the language's extension, ask for a completion. | ||
| 10 | + | ||
| 11 | +## Watch out for | ||
| 12 | + | ||
| 13 | +- **The docs are ahead of the binary until that re-pin**: an editor built from the current pin still has the defect the new variant says is gone. | ||
added
.memory/handoffs/2026-09-19-rickub-release-workflow.md +25 -0 | new file mode 100644 | ||
| @@ -0,0 +1,25 @@ | ||
| 1 | +# Handoff — 2026-09-19 — Rickub migration and the Release workflow | |
| 2 | + | |
| 3 | +## State | |
| 4 | + | |
| 5 | +- Module `rickub.com/turbo-editors/turbo-golo`, pinned to `rickub.com/turbo-editors/turbo-core v1.0.0`; `GOWORK=off make check` green. | |
| 6 | +- Releases: `./01-release.tag.sh` tags; the tag push runs `.github/workflows/release.yml`, which builds with `./02-build-releases.sh` and publishes the page with the binaries. `02-release.publish.sh` and `04-release.upload-binaries.sh` are deleted. No token needed. | |
| 7 | +- **Nothing is committed.** Fresh `git init`, `origin` at `ssh://git@rickub.com/turbo-editors/turbo-golo.git`, no commit; the sandbox cannot reach the remote. | |
| 8 | + | |
| 9 | +## Next steps | |
| 10 | + | |
| 11 | +1. Check `release.env` (`TAG="v1.0.0"`, `ABOUT="Turbo Golo"`) and run `./01-release.tag.sh` from a machine that reaches Rickub. It makes the root commit, pushes `main`, tags, pushes the tag. | |
| 12 | +2. Watch the Release workflow on the Actions tab. turbo-go's identical workflow has run and published; this one has not yet. | |
| 13 | +3. If the publish step answers 403, the binaries are on the run page as the `turbo-golo-<tag>` artifact (14 days). | |
| 14 | +4. `turbo-golo.token.env` was deleted on 2026-09-19; nothing read it any more. | |
| 15 | + | |
| 16 | +## Traps | |
| 17 | + | |
| 18 | +- `go.work` beside this checkout (where there is one) points at `../turbo-core`. Check the *published* shape with `GOWORK=off`; the release tests already do for their children. | |
| 19 | +- Do not `go get …/turbo-core@v0.9.0` under the new path: the proxy has it, but its `go.mod` declares the Codeberg path. v1.0.0 is the first usable version. | |
| 20 | +- `release/` holds the Codeberg-era binaries (hundreds of MB). Gitignored; the tests skip it when copying the module. | |
| 21 | +- `sed -i` on this filesystem drops the execute bit; `chmod 755` the scripts after any such edit. | |
| 22 | + | |
| 23 | +## Later the same day — the four-question test went red, as designed | |
| 24 | + | |
| 25 | +The user's first `./01-release.tag.sh` stopped in `make check`: GoloScript v0.2.0 answers references, implementations and workspace symbols. Tests and docs (EN + FR, nine pages + README + summary) were revised to say the boundary is `typeDefinition` alone; see `history.md`. GoloScript v0.2.0 is installed in the sandbox at `/usr/local/bin/golo`, so the ten `…WithRealGoloLSP` tests now run here instead of skipping. Re-run `./01-release.tag.sh`. | |
| new file mode 100644 | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | +# Handoff — 2026-09-19 — Rickub migration and the Release workflow | ||
| 2 | + | ||
| 3 | +## State | ||
| 4 | + | ||
| 5 | +- Module `rickub.com/turbo-editors/turbo-golo`, pinned to `rickub.com/turbo-editors/turbo-core v1.0.0`; `GOWORK=off make check` green. | ||
| 6 | +- Releases: `./01-release.tag.sh` tags; the tag push runs `.github/workflows/release.yml`, which builds with `./02-build-releases.sh` and publishes the page with the binaries. `02-release.publish.sh` and `04-release.upload-binaries.sh` are deleted. No token needed. | ||
| 7 | +- **Nothing is committed.** Fresh `git init`, `origin` at `ssh://git@rickub.com/turbo-editors/turbo-golo.git`, no commit; the sandbox cannot reach the remote. | ||
| 8 | + | ||
| 9 | +## Next steps | ||
| 10 | + | ||
| 11 | +1. Check `release.env` (`TAG="v1.0.0"`, `ABOUT="Turbo Golo"`) and run `./01-release.tag.sh` from a machine that reaches Rickub. It makes the root commit, pushes `main`, tags, pushes the tag. | ||
| 12 | +2. Watch the Release workflow on the Actions tab. turbo-go's identical workflow has run and published; this one has not yet. | ||
| 13 | +3. If the publish step answers 403, the binaries are on the run page as the `turbo-golo-<tag>` artifact (14 days). | ||
| 14 | +4. `turbo-golo.token.env` was deleted on 2026-09-19; nothing read it any more. | ||
| 15 | + | ||
| 16 | +## Traps | ||
| 17 | + | ||
| 18 | +- `go.work` beside this checkout (where there is one) points at `../turbo-core`. Check the *published* shape with `GOWORK=off`; the release tests already do for their children. | ||
| 19 | +- Do not `go get …/turbo-core@v0.9.0` under the new path: the proxy has it, but its `go.mod` declares the Codeberg path. v1.0.0 is the first usable version. | ||
| 20 | +- `release/` holds the Codeberg-era binaries (hundreds of MB). Gitignored; the tests skip it when copying the module. | ||
| 21 | +- `sed -i` on this filesystem drops the execute bit; `chmod 755` the scripts after any such edit. | ||
| 22 | + | ||
| 23 | +## Later the same day — the four-question test went red, as designed | ||
| 24 | + | ||
| 25 | +The user's first `./01-release.tag.sh` stopped in `make check`: GoloScript v0.2.0 answers references, implementations and workspace symbols. Tests and docs (EN + FR, nine pages + README + summary) were revised to say the boundary is `typeDefinition` alone; see `history.md`. GoloScript v0.2.0 is installed in the sandbox at `/usr/local/bin/golo`, so the ten `…WithRealGoloLSP` tests now run here instead of skipping. Re-run `./01-release.tag.sh`. | ||
added
.memory/history.md +83 -0 | new file mode 100644 | ||
| @@ -0,0 +1,83 @@ | ||
| 1 | +# turbo-golo — history | |
| 2 | + | |
| 3 | +One dated entry per session, appended at the end. Never rewritten. | |
| 4 | + | |
| 5 | +## 2026-09-14 — Turbo Golo built, from an empty repository to a passing gate | |
| 6 | + | |
| 7 | +*(Written by the following session from what was left on disk: this session ended before writing its memory. The code, tests, docs and quality reports are its evidence; `/tmp/tg/FACTS.md`, its own notes, is the source of the facts below.)* | |
| 8 | + | |
| 9 | +- **Goal**: ticket 4, "create turbo-golo" — `/turbo-new-editor` for Golo, in this directory, against turbo-core v0.5.0. | |
| 10 | +- **Changes**: `go.mod` (turbo-core v0.5.0, no active `replace`), `main.go`, `internal/gololang/` (profile, three-file scanner, three starter templates, six test files), `Makefile`, `scripts/`, the four release scripts, `docs/{en,fr}/` (34 EN pages, 33 FR — the French tutorial was not yet written), `docs/diagrams/packages.drawio`, `demos/` with three programs, `.qlty/` and `.quality/`. | |
| 11 | +- **Decisions** (recorded in `summary.md` — the main ones): no root marker; `golo lsp` as the server, the interpreter itself; strings carried to their closing quote wherever it is, following the lexer; `Some`/`None`/`Ok`/`Err` as types; the lexer's own identifier predicate, emoji included; the builtins table held to the real server; Run first in the tools file and no Format/Lint because GoloScript has neither; six placeholders in eight tools. | |
| 12 | +- **Tests**: 250, all passing with `golo` v0.1.1 installed. Scanner invariants, one case per construct and per refusal, the template contract, the editor on a `SimulationScreen`, the drawio held to `go list`, the languages reference held to the scanner, and the real `golo lsp` driven end to end — completion on typed text, hover, definition, symbols, and a file that does not compile. | |
| 13 | +- **Quality**: run #1 FAIL (2 smells), run #2 PASS — 0/0/0, complexity 54. | |
| 14 | +- **Verified in a pty**: the tutorial run start to finish (`/tmp/tg/tut.raw`), every colour claim read back as SGR, the `Tools` menu appearing, `Run` asking for the script, the `//` diagnostic in the gutter and on the status bar. | |
| 15 | +- **Not recorded**: whether a mutation pass was run over the tests. See `summary.md`, "Not yet established". | |
| 16 | + | |
| 17 | +## 2026-09-14 (later) — finishing the cycle: French tutorial, README, memory, family | |
| 18 | + | |
| 19 | +- **Asked**: "continue le travail que tu avais en cours sur le turbo-golo editor". Reconstructed the state from disk: code green, gate PASS, one French page missing, no root README, no `.memory/`, family not updated. | |
| 20 | +- **Changes**: `docs/fr/tutorials/getting-started.md` (written from the English one, whose claims were re-read off `/tmp/tg/tut.raw` first); `README.md` at the root; this `.memory/` (README, summary, history, handoff). | |
| 21 | +- **Outside this repository**: turbo-core (`README.md`, both doc READMEs, both `test-without-publishing.md`, `profile/profile.go`'s comment, `.memory/summary.md`, plus a history entry and a handoff) now counts five editors; turbo-python and turbo-moonbit (`docs/{en,fr}/explanation/architecture.md`, plus a history entry and a handoff each) say "all five editors". turbo-core's `.go` strings swept for hardcoded language names — all innocent, no library change. | |
| 22 | +- **Tests**: `go build ./...`, `go vet ./...`, `go test ./...` — 250 passing, 0 skipped; `gofmt -l` clean. `go build ./profile` in turbo-core after the comment change. | |
| 23 | +- **Quality**: PASS, re-run at the end of the session with no code change. | |
| 24 | +- **Skill**: `turbo-new-editor` (kit source and installed copy) gained this cycle's lessons — write the `.memory/` skeleton at step 1 rather than at the end, count both doc trees before declaring the docs done, and the no-root-marker case. | |
| 25 | + | |
| 26 | +## 2026-09-15 — ACP agent windows, ported from turbo-go | |
| 27 | + | |
| 28 | +- **Goal**: carry the Agent Client Protocol support to this editor. The feature itself is turbo-core's — the protocol client, the conversation model, the window widget, the `Agent` menu and the permission dialog all live there. See turbo-core's history for the same date. | |
| 29 | +- **Changes**: `internal/*/acp.toml.tmpl`, embedded in `templates.go` and wired into `profile.Templates.Agents`. That is the entire code change — one file and one line. Plus six documentation pages (EN + FR: how-to, reference, explanation) and their index entries. | |
| 30 | +- **Decisions**: the example agent is `docker agent`, as in every other editor, because the protocol is the point and the agent is the user's choice; the one sentence in the starter file that is about **this** editor names its own fence, and a test holds it to that — a starter file copied from another editor and left naming that editor's language is the obvious way to get this port wrong. | |
| 31 | +- **Tests**: 5 new in `internal/*`: both blanks filled with no `%!` marker, the file loads back as exactly one agent, it explains its keys, it names this editor's own language, and creating it twice leaves the first alone. | |
| 32 | +- **Quality**: PASS 0/0/0. | |
| 33 | +- **Docs**: adapted rather than copied — the slug, the language, the fence, the build command and the language server all differ from turbo-go's, and each was replaced. | |
| 34 | +- Not committed. | |
| 35 | + | |
| 36 | +## 2026-09-15 (night) — slash commands and `@` mentions, documented | |
| 37 | + | |
| 38 | +- **Documentation and the starter file only in this repository**; the code is turbo-core's (see its `.memory/` of the same date). The user asked for the ACP changes that let an agent's commands be discovered the way Zed discovers them, then for `@` as a file selector. | |
| 39 | +- **Changes**: `docs/{en,fr}/reference/acp.md` — seven key rows for the list, a **Commands and mentions** section, the `session/prompt` and `available_commands_update` rows, the Limits bullet; `docs/{en,fr}/how-to/talk-to-an-agent.md` — "Use the agent's own commands" and "Point the agent at a file"; `docs/{en,fr}/explanation/agent-windows.md` — the "left out" bullet narrowed to images, two sections appended; the embedded `acp.toml.tmpl` — two key lines. Applied by one script across the five editors with an exactly-once anchor check. | |
| 40 | +- **Tests**: `go test ./...` green (the template change is a comment; the tests that read the created file still pass). | |
| 41 | +- **Ahead of the binary**: this repository pins turbo-core v0.7.0, which has none of this. The pages are true once turbo-core is tagged and the pin moved. | |
| 42 | +- Not committed. | |
| 43 | + | |
| 44 | +## 2026-09-16 — the trace variable and a troubleshooting bullet, documented | |
| 45 | + | |
| 46 | +- turbo-core gained `TURBO_ACP_TRACE=<file>` and an "update this editor could not read" line in Agent status, because the user saw no `/` commands from their own agent and nothing on screen could say why. Documented here EN + FR: a bullet in the how-to's Variants, a section in `reference/acp.md`. Docs only; not committed. | |
| 47 | +- **Later on 2026-09-16**: `demos/.turbo-golo/acp.toml` (and `agent.yaml`, docker agent's config copied from turbo-go) now hold two agents — **Bob (llama.cpp)** via `docker agent serve acp`, and the user's **mini-me (llama.cpp)** (`mm -acp`, `AGENT_CONFIG` env) — placed where this repository's demo project already keeps its settings. Verified to load as two agents with this editor's own `Profile()`; not opened, `mm` and `docker` are on the user's Mac. Working files for trying the `/` picker, not part of the feature. | |
| 48 | + | |
| 49 | +## 2026-09-16 — family count: a sixth editor, Turbo JS | |
| 50 | + | |
| 51 | +- **Asked**: nothing of this repository directly. Turbo JS was built beside it, and the sentences here that count the family went false the moment it existed. | |
| 52 | +- **Changes**: `docs/en/explanation/architecture.md`, `docs/fr/explanation/architecture.md`, `docs/fr/explanation/design-decisions.md` — "five editors" → six, and the five scanners a rejected alternative would have moved into the library → six. Nothing else touched; history left as it was. | |
| 53 | +- **Tests**: none affected — documentation only. | |
| 54 | + | |
| 55 | +## 2026-09-17 — documentation: terminal windows and tools on Windows | |
| 56 | + | |
| 57 | +- **Asked**: nothing of this repository directly. turbo-core gained pseudo-console (ConPTY) terminal windows and a per-platform tools shell (cmd.exe on Windows); the pages here that said "Linux and macOS" or `/bin/sh -c` went false the moment that landed. | |
| 58 | +- **Changes** (EN + FR): `README.md` (terminal windows: Linux, macOS and Windows), `docs/*/reference/terminal.md` (shell row, controlling-terminal row, platform table, error row), `docs/*/explanation/terminal-windows.md` (the Windows section rewritten: a pseudo-console and why it is a file of its own, built and vetted but not yet run), `docs/*/how-to/use-a-terminal.md` (`%COMSPEC%`, the five things to try first on Windows), `docs/*/reference/golo-tools.md` (shell row, cmd.exe's globs and `;`, error row), `docs/*/explanation/golo-tools.md` (`cmd.exe /S /C`). Applied by one script across the six editors, one anchor per file. | |
| 59 | +- **Not changed**: code, `go.mod`. **The documentation is ahead of the binary** until turbo-core is tagged (v0.9.0) and re-pinned here; the feature has never been run on Windows by anyone. | |
| 60 | +- **Tests**: none affected — documentation only. | |
| 61 | + | |
| 62 | +## 2026-09-18 — the "Untitled window has no LSP" fix: docs variant added, fix is turbo-core's | |
| 63 | + | |
| 64 | +- **Asked**: propagate to every editor the fix made in turbo-core the same day — saving now announces a document the server does not know (a window that started Untitled gets LSP from its first save), and Save As under a new name closes the old document. See turbo-core's `.memory/history.md` of 2026-09-18 for the defect and the fix. | |
| 65 | +- **Changes here**: `docs/{en,fr}/how-to/enable-completion.md` gain one variant — completion in a window that started without a name works from its first save, no relaunch needed. No code in this repository is involved. | |
| 66 | +- **Quality**: gate not re-run — a Markdown-only change; the gate measures the Go code. | |
| 67 | +- **Docs ahead of the binary**: the behaviour arrives only when turbo-core is tagged and this editor re-pinned. Not committed. | |
| 68 | + | |
| 69 | +## 2026-09-19 — moved to Rickub: turbo-core v1.0.0 re-pinned, releases published by a workflow | |
| 70 | + | |
| 71 | +- **Asked**: the same migration turbo-go received the same day, for all five remaining editors — turbo-core moved to `rickub.com` and was published as v1.0.0 with a Release workflow. | |
| 72 | +- **Changes, module**: `codeberg.org/turbo-editors` → `rickub.com/turbo-editors` in `go.mod`, every `.go` file, `Makefile`, `scripts/install.sh`, `README.md`, `docs/{en,fr}`, `.memory/summary.md` (turbo-core deep links also from Codeberg's `src/branch/main/` to `blob/main/`). `require rickub.com/turbo-editors/turbo-core v1.0.0`; `go mod tidy` with `GOWORK=off` rewrote `go.sum` from the proxy. | |
| 73 | +- **Changes, release tooling**: `.github/workflows/release.yml` (new), `01-release.tag.sh` (rewritten), `03-build-releases.sh` → `02-build-releases.sh` (tag from `$1`, validation, `replace` check, fresh `release/${TAG}/`, `go install` line in the README, no hand-off to 04), `02-release.publish.sh` and `04-release.upload-binaries.sh` deleted, `release.env` rewritten (`TAG="v1.0.0"`, `ABOUT="Turbo Golo"`). All generated from turbo-go's final files with the names substituted; the README paragraph naming this editor's language server and the example file (`main.golo`) kept from the old 03. Docs: the release section and the wrong-commit variant of `docs/{en,fr}/how-to/make-a-release.md` rewritten. | |
| 74 | +- **Tests**: `release_test.go` regenerated from turbo-go's — see `summary.md`. The file-writing helper is `writeTestFile` because turbo-python's `main_test.go` owns `writeFile`, and the command helper `runOrFail` because `main.go` owns `run`. | |
| 75 | +- **Not done**: nothing committed or pushed — no commit exists yet and `origin` is unreachable from the sandbox. The workflow has not run on Rickub for this editor; turbo-go's identical one has, and published. | |
| 76 | + | |
| 77 | +## 2026-09-19 (later) — GoloScript v0.2.0 answers references, implementations and project symbols: the pinning test did its job | |
| 78 | + | |
| 79 | +- **Origin**: the user ran `./01-release.tag.sh`; its `make check` failed on `TestGoloLSPAnswersNoneOfTheFourQuestionsItDoesNotAdvertise` — `golo lsp` on their Mac now answers references, implementations and workspace symbols. Reproduced in the sandbox by installing GoloScript v0.2.0 (2026-09-14, `golo-v0.2.0-linux-arm64` from Codeberg) into `/usr/local/bin`: the same three assertions fail, type definition still answers nothing. The test had skipped itself here until then, `golo` not being installed. | |
| 80 | +- **Measured** against the server directly (`initialize` capabilities and a two-file project): it advertises `definitionProvider`, `diagnosticProvider`, `documentSymbolProvider`, `hoverProvider`, `implementationProvider`, `referencesProvider`, `workspaceSymbolProvider` — not `typeDefinitionProvider`, and `textDocument/typeDefinition` returns `-32601 unsupported method`. References: declaration + every call, **within the file only** (a call in another file of the project is not found). Implementation: the declaration itself. `workspace/symbol`: every `.golo` under the root, open or not, module names included; empty query lists all. Substring matching was not checked and is not claimed anywhere. | |
| 81 | +- **Changes, tests**: the four-question test replaced by `TestFindReferencesWithRealGoloLSP` (3 locations: lines 3, 8, 12), `TestFindImplementationsWithRealGoloLSP` (the declaration), `TestSymbolsAcrossTheProjectWithRealGoloLSP` (`helper`, and `elsewhere` in a file written after start and never opened) and `TestGoloLSPDoesNotAnswerTypeDefinitionWithRealGoloLSP`; new constant `secondCallLine = 12`. Ten real-server tests, all green here against v0.2.0. | |
| 82 | +- **Changes, docs** (EN + FR): `how-to/enable-completion.md` (table, the “without a key” sentence, the boundary paragraph), `reference/keyboard.md`, `reference/menus.md` (intro and three rows), `how-to/ask-about-code.md` (table, paragraph, the `No … found` row, the `Ctrl-T` bullet), `how-to/navigate-code.md`, `explanation/colouring-and-completion.md`, `explanation/design-decisions.md`, `explanation/architecture.md` (ten tests, two new bullets, the closing paragraph), `how-to/run-the-tests.md`; `README.md` feature bullet; `.memory/summary.md` two bullets. Each page now says the boundary is `typeDefinition` alone and that the test is how the other three were noticed. | |
| 83 | +- **Not done**: not committed — `01-release.tag.sh` is to be run again by the user. `diagnosticProvider` (pull diagnostics) is newly advertised too; turbo-core reads `publishDiagnostics` only, and the diagnostics tests still pass, so the server also pushes. Not investigated further. | |
| new file mode 100644 | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | +# turbo-golo — history | ||
| 2 | + | ||
| 3 | +One dated entry per session, appended at the end. Never rewritten. | ||
| 4 | + | ||
| 5 | +## 2026-09-14 — Turbo Golo built, from an empty repository to a passing gate | ||
| 6 | + | ||
| 7 | +*(Written by the following session from what was left on disk: this session ended before writing its memory. The code, tests, docs and quality reports are its evidence; `/tmp/tg/FACTS.md`, its own notes, is the source of the facts below.)* | ||
| 8 | + | ||
| 9 | +- **Goal**: ticket 4, "create turbo-golo" — `/turbo-new-editor` for Golo, in this directory, against turbo-core v0.5.0. | ||
| 10 | +- **Changes**: `go.mod` (turbo-core v0.5.0, no active `replace`), `main.go`, `internal/gololang/` (profile, three-file scanner, three starter templates, six test files), `Makefile`, `scripts/`, the four release scripts, `docs/{en,fr}/` (34 EN pages, 33 FR — the French tutorial was not yet written), `docs/diagrams/packages.drawio`, `demos/` with three programs, `.qlty/` and `.quality/`. | ||
| 11 | +- **Decisions** (recorded in `summary.md` — the main ones): no root marker; `golo lsp` as the server, the interpreter itself; strings carried to their closing quote wherever it is, following the lexer; `Some`/`None`/`Ok`/`Err` as types; the lexer's own identifier predicate, emoji included; the builtins table held to the real server; Run first in the tools file and no Format/Lint because GoloScript has neither; six placeholders in eight tools. | ||
| 12 | +- **Tests**: 250, all passing with `golo` v0.1.1 installed. Scanner invariants, one case per construct and per refusal, the template contract, the editor on a `SimulationScreen`, the drawio held to `go list`, the languages reference held to the scanner, and the real `golo lsp` driven end to end — completion on typed text, hover, definition, symbols, and a file that does not compile. | ||
| 13 | +- **Quality**: run #1 FAIL (2 smells), run #2 PASS — 0/0/0, complexity 54. | ||
| 14 | +- **Verified in a pty**: the tutorial run start to finish (`/tmp/tg/tut.raw`), every colour claim read back as SGR, the `Tools` menu appearing, `Run` asking for the script, the `//` diagnostic in the gutter and on the status bar. | ||
| 15 | +- **Not recorded**: whether a mutation pass was run over the tests. See `summary.md`, "Not yet established". | ||
| 16 | + | ||
| 17 | +## 2026-09-14 (later) — finishing the cycle: French tutorial, README, memory, family | ||
| 18 | + | ||
| 19 | +- **Asked**: "continue le travail que tu avais en cours sur le turbo-golo editor". Reconstructed the state from disk: code green, gate PASS, one French page missing, no root README, no `.memory/`, family not updated. | ||
| 20 | +- **Changes**: `docs/fr/tutorials/getting-started.md` (written from the English one, whose claims were re-read off `/tmp/tg/tut.raw` first); `README.md` at the root; this `.memory/` (README, summary, history, handoff). | ||
| 21 | +- **Outside this repository**: turbo-core (`README.md`, both doc READMEs, both `test-without-publishing.md`, `profile/profile.go`'s comment, `.memory/summary.md`, plus a history entry and a handoff) now counts five editors; turbo-python and turbo-moonbit (`docs/{en,fr}/explanation/architecture.md`, plus a history entry and a handoff each) say "all five editors". turbo-core's `.go` strings swept for hardcoded language names — all innocent, no library change. | ||
| 22 | +- **Tests**: `go build ./...`, `go vet ./...`, `go test ./...` — 250 passing, 0 skipped; `gofmt -l` clean. `go build ./profile` in turbo-core after the comment change. | ||
| 23 | +- **Quality**: PASS, re-run at the end of the session with no code change. | ||
| 24 | +- **Skill**: `turbo-new-editor` (kit source and installed copy) gained this cycle's lessons — write the `.memory/` skeleton at step 1 rather than at the end, count both doc trees before declaring the docs done, and the no-root-marker case. | ||
| 25 | + | ||
| 26 | +## 2026-09-15 — ACP agent windows, ported from turbo-go | ||
| 27 | + | ||
| 28 | +- **Goal**: carry the Agent Client Protocol support to this editor. The feature itself is turbo-core's — the protocol client, the conversation model, the window widget, the `Agent` menu and the permission dialog all live there. See turbo-core's history for the same date. | ||
| 29 | +- **Changes**: `internal/*/acp.toml.tmpl`, embedded in `templates.go` and wired into `profile.Templates.Agents`. That is the entire code change — one file and one line. Plus six documentation pages (EN + FR: how-to, reference, explanation) and their index entries. | ||
| 30 | +- **Decisions**: the example agent is `docker agent`, as in every other editor, because the protocol is the point and the agent is the user's choice; the one sentence in the starter file that is about **this** editor names its own fence, and a test holds it to that — a starter file copied from another editor and left naming that editor's language is the obvious way to get this port wrong. | ||
| 31 | +- **Tests**: 5 new in `internal/*`: both blanks filled with no `%!` marker, the file loads back as exactly one agent, it explains its keys, it names this editor's own language, and creating it twice leaves the first alone. | ||
| 32 | +- **Quality**: PASS 0/0/0. | ||
| 33 | +- **Docs**: adapted rather than copied — the slug, the language, the fence, the build command and the language server all differ from turbo-go's, and each was replaced. | ||
| 34 | +- Not committed. | ||
| 35 | + | ||
| 36 | +## 2026-09-15 (night) — slash commands and `@` mentions, documented | ||
| 37 | + | ||
| 38 | +- **Documentation and the starter file only in this repository**; the code is turbo-core's (see its `.memory/` of the same date). The user asked for the ACP changes that let an agent's commands be discovered the way Zed discovers them, then for `@` as a file selector. | ||
| 39 | +- **Changes**: `docs/{en,fr}/reference/acp.md` — seven key rows for the list, a **Commands and mentions** section, the `session/prompt` and `available_commands_update` rows, the Limits bullet; `docs/{en,fr}/how-to/talk-to-an-agent.md` — "Use the agent's own commands" and "Point the agent at a file"; `docs/{en,fr}/explanation/agent-windows.md` — the "left out" bullet narrowed to images, two sections appended; the embedded `acp.toml.tmpl` — two key lines. Applied by one script across the five editors with an exactly-once anchor check. | ||
| 40 | +- **Tests**: `go test ./...` green (the template change is a comment; the tests that read the created file still pass). | ||
| 41 | +- **Ahead of the binary**: this repository pins turbo-core v0.7.0, which has none of this. The pages are true once turbo-core is tagged and the pin moved. | ||
| 42 | +- Not committed. | ||
| 43 | + | ||
| 44 | +## 2026-09-16 — the trace variable and a troubleshooting bullet, documented | ||
| 45 | + | ||
| 46 | +- turbo-core gained `TURBO_ACP_TRACE=<file>` and an "update this editor could not read" line in Agent status, because the user saw no `/` commands from their own agent and nothing on screen could say why. Documented here EN + FR: a bullet in the how-to's Variants, a section in `reference/acp.md`. Docs only; not committed. | ||
| 47 | +- **Later on 2026-09-16**: `demos/.turbo-golo/acp.toml` (and `agent.yaml`, docker agent's config copied from turbo-go) now hold two agents — **Bob (llama.cpp)** via `docker agent serve acp`, and the user's **mini-me (llama.cpp)** (`mm -acp`, `AGENT_CONFIG` env) — placed where this repository's demo project already keeps its settings. Verified to load as two agents with this editor's own `Profile()`; not opened, `mm` and `docker` are on the user's Mac. Working files for trying the `/` picker, not part of the feature. | ||
| 48 | + | ||
| 49 | +## 2026-09-16 — family count: a sixth editor, Turbo JS | ||
| 50 | + | ||
| 51 | +- **Asked**: nothing of this repository directly. Turbo JS was built beside it, and the sentences here that count the family went false the moment it existed. | ||
| 52 | +- **Changes**: `docs/en/explanation/architecture.md`, `docs/fr/explanation/architecture.md`, `docs/fr/explanation/design-decisions.md` — "five editors" → six, and the five scanners a rejected alternative would have moved into the library → six. Nothing else touched; history left as it was. | ||
| 53 | +- **Tests**: none affected — documentation only. | ||
| 54 | + | ||
| 55 | +## 2026-09-17 — documentation: terminal windows and tools on Windows | ||
| 56 | + | ||
| 57 | +- **Asked**: nothing of this repository directly. turbo-core gained pseudo-console (ConPTY) terminal windows and a per-platform tools shell (cmd.exe on Windows); the pages here that said "Linux and macOS" or `/bin/sh -c` went false the moment that landed. | ||
| 58 | +- **Changes** (EN + FR): `README.md` (terminal windows: Linux, macOS and Windows), `docs/*/reference/terminal.md` (shell row, controlling-terminal row, platform table, error row), `docs/*/explanation/terminal-windows.md` (the Windows section rewritten: a pseudo-console and why it is a file of its own, built and vetted but not yet run), `docs/*/how-to/use-a-terminal.md` (`%COMSPEC%`, the five things to try first on Windows), `docs/*/reference/golo-tools.md` (shell row, cmd.exe's globs and `;`, error row), `docs/*/explanation/golo-tools.md` (`cmd.exe /S /C`). Applied by one script across the six editors, one anchor per file. | ||
| 59 | +- **Not changed**: code, `go.mod`. **The documentation is ahead of the binary** until turbo-core is tagged (v0.9.0) and re-pinned here; the feature has never been run on Windows by anyone. | ||
| 60 | +- **Tests**: none affected — documentation only. | ||
| 61 | + | ||
| 62 | +## 2026-09-18 — the "Untitled window has no LSP" fix: docs variant added, fix is turbo-core's | ||
| 63 | + | ||
| 64 | +- **Asked**: propagate to every editor the fix made in turbo-core the same day — saving now announces a document the server does not know (a window that started Untitled gets LSP from its first save), and Save As under a new name closes the old document. See turbo-core's `.memory/history.md` of 2026-09-18 for the defect and the fix. | ||
| 65 | +- **Changes here**: `docs/{en,fr}/how-to/enable-completion.md` gain one variant — completion in a window that started without a name works from its first save, no relaunch needed. No code in this repository is involved. | ||
| 66 | +- **Quality**: gate not re-run — a Markdown-only change; the gate measures the Go code. | ||
| 67 | +- **Docs ahead of the binary**: the behaviour arrives only when turbo-core is tagged and this editor re-pinned. Not committed. | ||
| 68 | + | ||
| 69 | +## 2026-09-19 — moved to Rickub: turbo-core v1.0.0 re-pinned, releases published by a workflow | ||
| 70 | + | ||
| 71 | +- **Asked**: the same migration turbo-go received the same day, for all five remaining editors — turbo-core moved to `rickub.com` and was published as v1.0.0 with a Release workflow. | ||
| 72 | +- **Changes, module**: `codeberg.org/turbo-editors` → `rickub.com/turbo-editors` in `go.mod`, every `.go` file, `Makefile`, `scripts/install.sh`, `README.md`, `docs/{en,fr}`, `.memory/summary.md` (turbo-core deep links also from Codeberg's `src/branch/main/` to `blob/main/`). `require rickub.com/turbo-editors/turbo-core v1.0.0`; `go mod tidy` with `GOWORK=off` rewrote `go.sum` from the proxy. | ||
| 73 | +- **Changes, release tooling**: `.github/workflows/release.yml` (new), `01-release.tag.sh` (rewritten), `03-build-releases.sh` → `02-build-releases.sh` (tag from `$1`, validation, `replace` check, fresh `release/${TAG}/`, `go install` line in the README, no hand-off to 04), `02-release.publish.sh` and `04-release.upload-binaries.sh` deleted, `release.env` rewritten (`TAG="v1.0.0"`, `ABOUT="Turbo Golo"`). All generated from turbo-go's final files with the names substituted; the README paragraph naming this editor's language server and the example file (`main.golo`) kept from the old 03. Docs: the release section and the wrong-commit variant of `docs/{en,fr}/how-to/make-a-release.md` rewritten. | ||
| 74 | +- **Tests**: `release_test.go` regenerated from turbo-go's — see `summary.md`. The file-writing helper is `writeTestFile` because turbo-python's `main_test.go` owns `writeFile`, and the command helper `runOrFail` because `main.go` owns `run`. | ||
| 75 | +- **Not done**: nothing committed or pushed — no commit exists yet and `origin` is unreachable from the sandbox. The workflow has not run on Rickub for this editor; turbo-go's identical one has, and published. | ||
| 76 | + | ||
| 77 | +## 2026-09-19 (later) — GoloScript v0.2.0 answers references, implementations and project symbols: the pinning test did its job | ||
| 78 | + | ||
| 79 | +- **Origin**: the user ran `./01-release.tag.sh`; its `make check` failed on `TestGoloLSPAnswersNoneOfTheFourQuestionsItDoesNotAdvertise` — `golo lsp` on their Mac now answers references, implementations and workspace symbols. Reproduced in the sandbox by installing GoloScript v0.2.0 (2026-09-14, `golo-v0.2.0-linux-arm64` from Codeberg) into `/usr/local/bin`: the same three assertions fail, type definition still answers nothing. The test had skipped itself here until then, `golo` not being installed. | ||
| 80 | +- **Measured** against the server directly (`initialize` capabilities and a two-file project): it advertises `definitionProvider`, `diagnosticProvider`, `documentSymbolProvider`, `hoverProvider`, `implementationProvider`, `referencesProvider`, `workspaceSymbolProvider` — not `typeDefinitionProvider`, and `textDocument/typeDefinition` returns `-32601 unsupported method`. References: declaration + every call, **within the file only** (a call in another file of the project is not found). Implementation: the declaration itself. `workspace/symbol`: every `.golo` under the root, open or not, module names included; empty query lists all. Substring matching was not checked and is not claimed anywhere. | ||
| 81 | +- **Changes, tests**: the four-question test replaced by `TestFindReferencesWithRealGoloLSP` (3 locations: lines 3, 8, 12), `TestFindImplementationsWithRealGoloLSP` (the declaration), `TestSymbolsAcrossTheProjectWithRealGoloLSP` (`helper`, and `elsewhere` in a file written after start and never opened) and `TestGoloLSPDoesNotAnswerTypeDefinitionWithRealGoloLSP`; new constant `secondCallLine = 12`. Ten real-server tests, all green here against v0.2.0. | ||
| 82 | +- **Changes, docs** (EN + FR): `how-to/enable-completion.md` (table, the “without a key” sentence, the boundary paragraph), `reference/keyboard.md`, `reference/menus.md` (intro and three rows), `how-to/ask-about-code.md` (table, paragraph, the `No … found` row, the `Ctrl-T` bullet), `how-to/navigate-code.md`, `explanation/colouring-and-completion.md`, `explanation/design-decisions.md`, `explanation/architecture.md` (ten tests, two new bullets, the closing paragraph), `how-to/run-the-tests.md`; `README.md` feature bullet; `.memory/summary.md` two bullets. Each page now says the boundary is `typeDefinition` alone and that the test is how the other three were noticed. | ||
| 83 | +- **Not done**: not committed — `01-release.tag.sh` is to be run again by the user. `diagnosticProvider` (pull diagnostics) is newly advertised too; turbo-core reads `publishDiagnostics` only, and the diagnostics tests still pass, so the server also pushes. Not investigated further. | ||
added
.memory/summary.md +106 -0 | new file mode 100644 | ||
| @@ -0,0 +1,106 @@ | ||
| 1 | +# turbo-golo — summary | |
| 2 | + | |
| 3 | +A snapshot of the present. Edited in place; the history is in `history.md`. | |
| 4 | + | |
| 5 | +## What this is | |
| 6 | + | |
| 7 | +A Turbo C-style terminal IDE for **Golo**, written in Go, built on **[turbo-core](https://rickub.com/turbo-editors/turbo-core)** — the library Turbo Go, Turbo Rust, Turbo Python and Turbo MoonBit already share. This repository holds the command, the profile that says the editor is for Golo, and the Golo scanner. Everything else — the event loop, the windows, the dialogs, the themes, the LSP client, the terminal emulator, the project tree, the snippets and tools machinery — is the library's, and none of it is copied here. | |
| 8 | + | |
| 9 | +**Golo is the language; GoloScript is the implementation.** Golo was created for the JVM (Eclipse Golo); GoloScript is the independent reimplementation in Go — `https://codeberg.org/TypeUnsafe/golo-script` — whose `golo` binary this editor talks to. The docs say "Golo" for the language and its files, "GoloScript" for the implementation and its toolchain, and `golo` for the binary. Never "GoloScript file". | |
| 10 | + | |
| 11 | +Module `rickub.com/turbo-editors/turbo-golo`, `require`ing turbo-core **v0.5.0** from the module proxy with **no active `replace`**. The commented-out `replace` at the bottom of `go.mod` documents the escape hatch without being one; `01-release.tag.sh` refuses to tag a release whose `go.mod` carries a live one. | |
| 12 | + | |
| 13 | +## Layout | |
| 14 | + | |
| 15 | +| | | | |
| 16 | +| --- | --- | | |
| 17 | +| `main.go` | flags (`-theme`, `-list-themes`, `-no-lsp`, `-version`), the terminal, `gololang.Register()`, the profile, the loop | | |
| 18 | +| `internal/gololang/gololang.go` | `Name`, `Slug`, `Language`, `Profile()`, `Register()`, `ServerArgs()`, `ServerDirs()` | | |
| 19 | +| `internal/gololang/scan.go` | the dispatcher, `#` and `----` comments, the carry | | |
| 20 | +| `internal/gololang/literals.go` | `"…"`, `"""…"""`, `'c'` | | |
| 21 | +| `internal/gololang/words.go` | numbers, keywords, constants, builtins, the capital-letter rule, the name after `function`, the path after `module`/`import` | | |
| 22 | +| `internal/gololang/*.toml.tmpl` | the three starter files, embedded by `templates.go` | | |
| 23 | +| `diagram_test.go` | holds `docs/diagrams/packages.drawio` to `go list` | | |
| 24 | +| `internal/gololang/reference_test.go` | holds `docs/*/reference/languages.md` to the scanner | | |
| 25 | +| `internal/gololang/editor_test.go` | the editor assembled on a `SimulationScreen`, and the real `golo lsp` driven end to end | | |
| 26 | +| `docs/{en,fr}/` | 34 pages each (README included), Diátaxis | | |
| 27 | +| `demos/` | three Golo programs: `hello`, `shapes` (with `shapes_test.golo`), `syntax-tour` (`tour.golo` runs; `lexer-only.golo` is coloured but does not) | | |
| 28 | + | |
| 29 | +## How to build, test and measure | |
| 30 | + | |
| 31 | +```bash | |
| 32 | +make check # fmt, vet, then the whole suite — what a commit should pass | |
| 33 | +make build # into bin/turbo-golo, then scripts/check-version.sh on the binary | |
| 34 | +make install # build, install onto PATH, report what it found (runs golo lsp to check the server) | |
| 35 | +go test ./... # the golo-lsp tests skip themselves without golo on PATH, and under -short | |
| 36 | +``` | |
| 37 | + | |
| 38 | +Quality gate, separate from the tests: | |
| 39 | + | |
| 40 | +```bash | |
| 41 | +python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace . | |
| 42 | +``` | |
| 43 | + | |
| 44 | +To build against a turbo-core you have changed but not released: | |
| 45 | + | |
| 46 | +```bash | |
| 47 | +go work init . ../turbo-core | |
| 48 | +go list -f '{{.Dir}}' rickub.com/turbo-editors/turbo-core/app # must NOT be under pkg/mod | |
| 49 | +``` | |
| 50 | + | |
| 51 | +`go.work` and `go.work.sum` are gitignored. **Everything still builds and still passes** while testing the published library instead of your changes, so run that second line. | |
| 52 | + | |
| 53 | +## Decisions in force | |
| 54 | + | |
| 55 | +- **No root marker.** `RootMarkers` is `nil`, the only editor in the family where it is. Golo has no manifest — a script is a file, a program is a directory of them — so `app.ProjectRoot` answers with the directory of the file being edited, which is also all `golo lsp` needs: it resolves imports from the modules embedded in the binary, never from disk. A marker would make a walk look like part of a rule that does not exist. | |
| 56 | +- **The language server is the interpreter**: `golo lsp`, over stdio. Nothing separate to install. The `lsp` argument is load-bearing — `golo` with no argument starts its REPL and reads stdin as Golo, which the editor would see as a server that answers nothing and never dies. Searched on `PATH`, then `/usr/local/bin` (where GoloScript's `install.sh` writes); there is no per-user toolchain directory and no environment variable naming one, so nothing else is listed. | |
| 57 | +- **`golo lsp` answers eight of turbo-core's nine questions since GoloScript v0.2.0** — completion, hover, definition, documentSymbol, references, implementation, workspace/symbol — and publishes diagnostics unasked. It does not advertise typeDefinition (`unsupported method`). References and implementations are within the file, and an implementation is the declaration itself (Golo has no interfaces); workspace/symbol searches every `.golo` under the root, open or not, module names included, and an empty query lists everything. Until v0.2.0 only the first four were advertised, and `TestGoloLSPAnswersNoneOfTheFourQuestionsItDoesNotAdvertise` went red on 2026-09-19 exactly as designed; it is now three positive tests plus `TestGoloLSPDoesNotAnswerTypeDefinitionWithRealGoloLSP`. Its diagnostics include two lints beyond syntax errors: `:`/`.` confusion, and C-style `//` and `/* */` comments — the tutorial's Step 8 uses the second. | |
| 58 | +- **The toolchain menu is `~G~olo`, `Alt-G`.** G is free (fixed menus take F, E, S, R, C, O, W, N, H). Named after the language, not after `golo`, `gogolo` or `wagolo`. | |
| 59 | +- **`Language` is `"Golo"`**, what the About box and status bar read out. The registry name is `golo`, lower case like every other entry. | |
| 60 | +- **Strings are carried across lines to the closing quote**, the opposite of Turbo MoonBit's decision, for the opposite reason: GoloScript's lexer reads them that way, so an unterminated string colours the rest of the file until a quote turns up, and that colour is a true statement about what the interpreter will read. Character literals `'c'` are carried the same way; `"""…"""` has no escapes inside. `----` block comments cross lines without nesting; three dashes are an operator run. | |
| 61 | +- **Numbers follow the lexer, not intuition.** `.digits` only when a digit follows the point, so `1..3` is a number and a range; `e`/`E` exponent; suffixes `L`, `F`, `f`. **No** hexadecimal, binary, octal or `_` separators — `0xFF` is `0` then the name `xFF` — because the lexer has none. | |
| 62 | +- **A capitalised name is a type** (struct, union, variant, augmentation target). **`Some`, `None`, `Ok` and `Err` are types, not constants** — in Golo they are variants of ordinary unions from `gololang.Errors`, available only after an import, and colouring them as built in would say otherwise. The name after `function` is a function by position; the dotted path after `module` or `import` is one type span. | |
| 63 | +- **Names use the lexer's own predicate**, not turbo-core's ASCII one: any Unicode letter or mark, `_`, and four emoji blocks. `let 😀 = 1` and `function 🚀launch = …` are coloured, and so are `été` and `名前`. | |
| 64 | +- **The builtins table is held to the real `golo lsp`** by a test — the 157 names `evaluator.BuiltinNames()` lists minus the five `__`-prefixed test counters — rather than remembered. 38 keywords, 3 constants. | |
| 65 | +- **The scanner colours what the lexer reads, and the parser (v0.1.1) refuses some of it**: `42L`, `3.14F`, `'c'`, `..`, `orIfNull`, `oftype`, `local function`. A coloured token is not a promise the interpreter accepts it; `reference/languages.md` says so and `demos/syntax-tour/lexer-only.golo` holds them. | |
| 66 | +- **There is no `golo fmt` and no `golo lint`.** The lints live in the language server, so the starter tools file has no Format or Lint entry — Run comes first, because for a scripting language "what does it print?" is the question asked most often. | |
| 67 | +- **Eight tools in the Golo menu plus `Echo` in a `Tools` menu**: Run, Test, Test one, Debug, REPL, New script, Build native (`gogolo`), Build wasm (`wagolo`). **Six use a `{{placeholder}}`**, more than any sibling, because with no manifest every command that touches a file has to be told which one. All commands were run against the real binaries. | |
| 68 | +- **Snippet bodies are TOML literal strings, indented two spaces.** Golo strings carry `\n` and `\"`, which TOML basic strings would resolve; two spaces is the convention in every GoloScript example, and Golo has no formatter to disagree with. | |
| 69 | +- **`autosave = true` in the starter settings file, `false` in `settings.Default()`.** Two statements in two places on purpose. | |
| 70 | +- **The starter templates are embedded files, not Go constants**, with the `.tmpl` suffix because `settings.toml.tmpl` holds `theme = %q`, which is not valid TOML. | |
| 71 | +- **A shebang naming `golo` is claimed** (`#!/usr/bin/env golo`): `#` opens a comment in Golo, so the interpreter reads the line as one. | |
| 72 | +- **Agent windows are turbo-core's, and what belongs here is the starter file.** `acp.toml.tmpl` is the fourth embedded template, and `profile.Templates.Agents` is the whole of Turbo Golo's contribution to the feature. The example agent is `docker agent serve acp .turbo-golo/agent.yaml`; the only other thing about this editor in it is the sentence saying a ```golo fence is coloured by the scanner this editor colours its own files with. Every other editor got agent windows the same way — one file, one line. The reasoning, and why it could not have been built here, is in `docs/*/explanation/agent-windows.md`. | |
| 73 | +- **The starter agents file teaches the window's keyboard as well as the format.** `Enter`, `Alt-Enter`, `Tab`, `Esc`, `Ctrl-W` and the copying keys are all in its comments, because a file the editor hands you is the one document a user is guaranteed to see. | |
| 74 | + | |
| 75 | +## State as of 2026-09-14 | |
| 76 | + | |
| 77 | +- **Complete and green, uncommitted.** `go build`, `go vet`, `gofmt -l` clean; **250 tests pass, 0 skipped** with `golo` v0.1.1 installed. The repository is at its initial commit `7069d2c` with everything untracked. | |
| 78 | +- **Quality gate: PASS**, run #2 (run #1 failed on 2 smells, fixed the same minute). 0 errors, 0 warnings, 0 smells; total complexity 54, worst file `words.go` at 29 against a limit of 60; 1002 lines, 491 of code. | |
| 79 | +- **Documentation**: 34 pages × EN + FR, a `README.md` at the root, `demos/README.md`, and `docs/diagrams/packages.drawio` checked against `go list`. The French tutorial was the last page written, on the second session of the day. | |
| 80 | +- **Verified in a real pty** (`/tmp/tg/tut.raw`, replayed with `/tmp/tg/screen.py`): the bar ` File Edit Search Run Code Options Window Snippets Golo Help`; the `Tools` menu appearing between Golo and Help once the tools file exists; `Run` asking `script, e.g. main.golo:` then a terminal window printing the three greetings; **a `// run it` line showing `×` (`91;44;1`) in the gutter and `⚠ GoloScript has no '//' line comments…` on the status bar**; the Options menu. Tutorial colours read back as SGR: keyword `97;44;1`, type `96;44`, function `93;44;1`, builtin `96;44;1`, identifier `93;44`, string `92;44`, comment `38;2;143;143;143`, operator `37;44`; numbers `95;44`. From File, `→` five times reaches Options and eight reaches Golo — read off the wire. | |
| 81 | +- **Registered in the family.** turbo-core's `README.md`, both doc READMEs, both workspace how-tos, `profile/profile.go`'s comment and `.memory/summary.md` count five editors; turbo-python's and turbo-moonbit's architecture pages say "all five editors". turbo-core's `.go` strings were swept for hardcoded language names against v0.5.0: every hit innocent, **no library change needed** — Turbo Golo is the first editor with no root marker and the library's fallback handled it unchanged. | |
| 82 | + | |
| 83 | +## Known boundaries | |
| 84 | + | |
| 85 | +- **Definition and document symbols are file-local**: top-level functions and unions, variants nested under their union. Imports of *user* modules on disk are not resolved by the server. | |
| 86 | +- **A diagnostic without a line number lands on line 1**, and the whole line is marked — the server gives no column. | |
| 87 | +- **`typeDefinition` reports nothing found**, because the server does not provide it; `references`, `implementation` and `workspace/symbol` are answered since GoloScript v0.2.0. | |
| 88 | + | |
| 89 | +## Not yet established | |
| 90 | + | |
| 91 | +- **Agent windows have never been opened in this editor.** The feature is turbo-core's and was driven end to end from turbo-go against a real `docker agent` and a real llama.cpp; what is here is the starter file, covered by tests that create it, load it back and check it names this editor's own language. Nobody has run `turbo-golo`, pressed `Alt-A` and talked to an agent from it. | |
| 92 | + | |
| 93 | + | |
| 94 | +- **No record that the tests were falsified by mutation.** The session that wrote them ended before writing this file, and left only `/tmp/tg/FACTS.md` behind. Every sibling's first session ran a mutation pass; whether this one did is unknown. Running one is the next thing worth doing before a release. | |
| 95 | +- **Never run on macOS or Windows.** Everything here was verified on Linux/arm64. A binary built in this sandbox is an ELF. | |
| 96 | +- **No CI.** There is no pipeline configuration in the repository. | |
| 97 | +- **Never released.** No tag exists; `release.env` says `TAG="v0.1.0"`, which is right for a first tag. `01`–`04` have only ever been read here, never run. | |
| 98 | +- **Performance on a large file is unmeasured**, and a string carried across many lines re-scans on every edit like any carry does. | |
| 99 | +- **Nothing checks the demos automatically.** They were run by hand under `golo` v0.1.1; a future GoloScript could change what the parser accepts without anything here noticing. | |
| 100 | +- **The `golo lsp` tests have only been run against v0.1.1.** The builtins table is held to that version's `BuiltinNames()`; a newer golo that adds a builtin will fail the test, which is the intent. | |
| 101 | + | |
| 102 | +## State as of 2026-09-19 — moved to Rickub, released by a workflow | |
| 103 | + | |
| 104 | +- **Module path `rickub.com/turbo-editors/turbo-golo`**, depending on `rickub.com/turbo-editors/turbo-core v1.0.0` — the first turbo-core version published under that path (`v0.9.0` on the proxy still declares the Codeberg path and cannot be required as `rickub.com/…`). Every import, the Makefile's `VERSION_PKG`, `scripts/install.sh`, the README and the docs say `rickub.com`. `GOWORK=off make check` green. The repository on this side is a fresh `git init` with `origin` at `ssh://git@rickub.com/turbo-editors/turbo-golo.git` and **no commit yet**; `01-release.tag.sh` makes the first one. | |
| 105 | +- **Releases are one script and one workflow**, modelled on turbo-core's and identical to turbo-go's. `01-release.tag.sh` runs `make check` under `TURBO_GOLO_RELEASING=1`, refuses a tag taken locally or on origin (bump, never move), refuses a `replace` in `go.mod`, commits, pushes the current branch, then tags and pushes the tag. That push starts `.github/workflows/release.yml`: `go test` with `TURBO_GOLO_RELEASING=1`, `./02-build-releases.sh "${GITHUB_REF_NAME}"`, release notes from the tag message, a run artifact, then `softprops/action-gh-release@v2` attaching `turbo-golo-*`, `SHA256SUMS` and `README.md` with the job's own `GITHUB_TOKEN` — the only credential Rickub's release API accepts. **`02-release.publish.sh` and `04-release.upload-binaries.sh` are gone**; the build script is now `02-build-releases.sh`, takes the tag as `$1` (CI has no `release.env`), validates it, refuses a `replace`, and starts from an empty `release/${TAG}/`. `release.env` holds only `TAG` and `ABOUT`; `turbo-golo.token.env` is read by nothing. | |
| 106 | +- **`release_test.go`** runs `01` for real against a throwaway bare remote (publishes, then refuses the same tag), runs `02` alone to see it refuse `v0.o.0`, and asserts the workflow's trigger, `contents: write`, `./02-build-releases.sh`, `fail_on_unmatched_files`, docs linked at the tag, no `secrets.`, and `TURBO_GOLO_RELEASING`. The copy of the module for the clone leaves out `.git`, `bin`, `release`, `kits`, `demo`, `demos`, `*.env` and `go.work*`; children run with `GOWORK=off`. | |
| new file mode 100644 | |||
| @@ -0,0 +1,106 @@ | |||
| 1 | +# turbo-golo — summary | ||
| 2 | + | ||
| 3 | +A snapshot of the present. Edited in place; the history is in `history.md`. | ||
| 4 | + | ||
| 5 | +## What this is | ||
| 6 | + | ||
| 7 | +A Turbo C-style terminal IDE for **Golo**, written in Go, built on **[turbo-core](https://rickub.com/turbo-editors/turbo-core)** — the library Turbo Go, Turbo Rust, Turbo Python and Turbo MoonBit already share. This repository holds the command, the profile that says the editor is for Golo, and the Golo scanner. Everything else — the event loop, the windows, the dialogs, the themes, the LSP client, the terminal emulator, the project tree, the snippets and tools machinery — is the library's, and none of it is copied here. | ||
| 8 | + | ||
| 9 | +**Golo is the language; GoloScript is the implementation.** Golo was created for the JVM (Eclipse Golo); GoloScript is the independent reimplementation in Go — `https://codeberg.org/TypeUnsafe/golo-script` — whose `golo` binary this editor talks to. The docs say "Golo" for the language and its files, "GoloScript" for the implementation and its toolchain, and `golo` for the binary. Never "GoloScript file". | ||
| 10 | + | ||
| 11 | +Module `rickub.com/turbo-editors/turbo-golo`, `require`ing turbo-core **v0.5.0** from the module proxy with **no active `replace`**. The commented-out `replace` at the bottom of `go.mod` documents the escape hatch without being one; `01-release.tag.sh` refuses to tag a release whose `go.mod` carries a live one. | ||
| 12 | + | ||
| 13 | +## Layout | ||
| 14 | + | ||
| 15 | +| | | | ||
| 16 | +| --- | --- | | ||
| 17 | +| `main.go` | flags (`-theme`, `-list-themes`, `-no-lsp`, `-version`), the terminal, `gololang.Register()`, the profile, the loop | | ||
| 18 | +| `internal/gololang/gololang.go` | `Name`, `Slug`, `Language`, `Profile()`, `Register()`, `ServerArgs()`, `ServerDirs()` | | ||
| 19 | +| `internal/gololang/scan.go` | the dispatcher, `#` and `----` comments, the carry | | ||
| 20 | +| `internal/gololang/literals.go` | `"…"`, `"""…"""`, `'c'` | | ||
| 21 | +| `internal/gololang/words.go` | numbers, keywords, constants, builtins, the capital-letter rule, the name after `function`, the path after `module`/`import` | | ||
| 22 | +| `internal/gololang/*.toml.tmpl` | the three starter files, embedded by `templates.go` | | ||
| 23 | +| `diagram_test.go` | holds `docs/diagrams/packages.drawio` to `go list` | | ||
| 24 | +| `internal/gololang/reference_test.go` | holds `docs/*/reference/languages.md` to the scanner | | ||
| 25 | +| `internal/gololang/editor_test.go` | the editor assembled on a `SimulationScreen`, and the real `golo lsp` driven end to end | | ||
| 26 | +| `docs/{en,fr}/` | 34 pages each (README included), Diátaxis | | ||
| 27 | +| `demos/` | three Golo programs: `hello`, `shapes` (with `shapes_test.golo`), `syntax-tour` (`tour.golo` runs; `lexer-only.golo` is coloured but does not) | | ||
| 28 | + | ||
| 29 | +## How to build, test and measure | ||
| 30 | + | ||
| 31 | +```bash | ||
| 32 | +make check # fmt, vet, then the whole suite — what a commit should pass | ||
| 33 | +make build # into bin/turbo-golo, then scripts/check-version.sh on the binary | ||
| 34 | +make install # build, install onto PATH, report what it found (runs golo lsp to check the server) | ||
| 35 | +go test ./... # the golo-lsp tests skip themselves without golo on PATH, and under -short | ||
| 36 | +``` | ||
| 37 | + | ||
| 38 | +Quality gate, separate from the tests: | ||
| 39 | + | ||
| 40 | +```bash | ||
| 41 | +python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace . | ||
| 42 | +``` | ||
| 43 | + | ||
| 44 | +To build against a turbo-core you have changed but not released: | ||
| 45 | + | ||
| 46 | +```bash | ||
| 47 | +go work init . ../turbo-core | ||
| 48 | +go list -f '{{.Dir}}' rickub.com/turbo-editors/turbo-core/app # must NOT be under pkg/mod | ||
| 49 | +``` | ||
| 50 | + | ||
| 51 | +`go.work` and `go.work.sum` are gitignored. **Everything still builds and still passes** while testing the published library instead of your changes, so run that second line. | ||
| 52 | + | ||
| 53 | +## Decisions in force | ||
| 54 | + | ||
| 55 | +- **No root marker.** `RootMarkers` is `nil`, the only editor in the family where it is. Golo has no manifest — a script is a file, a program is a directory of them — so `app.ProjectRoot` answers with the directory of the file being edited, which is also all `golo lsp` needs: it resolves imports from the modules embedded in the binary, never from disk. A marker would make a walk look like part of a rule that does not exist. | ||
| 56 | +- **The language server is the interpreter**: `golo lsp`, over stdio. Nothing separate to install. The `lsp` argument is load-bearing — `golo` with no argument starts its REPL and reads stdin as Golo, which the editor would see as a server that answers nothing and never dies. Searched on `PATH`, then `/usr/local/bin` (where GoloScript's `install.sh` writes); there is no per-user toolchain directory and no environment variable naming one, so nothing else is listed. | ||
| 57 | +- **`golo lsp` answers eight of turbo-core's nine questions since GoloScript v0.2.0** — completion, hover, definition, documentSymbol, references, implementation, workspace/symbol — and publishes diagnostics unasked. It does not advertise typeDefinition (`unsupported method`). References and implementations are within the file, and an implementation is the declaration itself (Golo has no interfaces); workspace/symbol searches every `.golo` under the root, open or not, module names included, and an empty query lists everything. Until v0.2.0 only the first four were advertised, and `TestGoloLSPAnswersNoneOfTheFourQuestionsItDoesNotAdvertise` went red on 2026-09-19 exactly as designed; it is now three positive tests plus `TestGoloLSPDoesNotAnswerTypeDefinitionWithRealGoloLSP`. Its diagnostics include two lints beyond syntax errors: `:`/`.` confusion, and C-style `//` and `/* */` comments — the tutorial's Step 8 uses the second. | ||
| 58 | +- **The toolchain menu is `~G~olo`, `Alt-G`.** G is free (fixed menus take F, E, S, R, C, O, W, N, H). Named after the language, not after `golo`, `gogolo` or `wagolo`. | ||
| 59 | +- **`Language` is `"Golo"`**, what the About box and status bar read out. The registry name is `golo`, lower case like every other entry. | ||
| 60 | +- **Strings are carried across lines to the closing quote**, the opposite of Turbo MoonBit's decision, for the opposite reason: GoloScript's lexer reads them that way, so an unterminated string colours the rest of the file until a quote turns up, and that colour is a true statement about what the interpreter will read. Character literals `'c'` are carried the same way; `"""…"""` has no escapes inside. `----` block comments cross lines without nesting; three dashes are an operator run. | ||
| 61 | +- **Numbers follow the lexer, not intuition.** `.digits` only when a digit follows the point, so `1..3` is a number and a range; `e`/`E` exponent; suffixes `L`, `F`, `f`. **No** hexadecimal, binary, octal or `_` separators — `0xFF` is `0` then the name `xFF` — because the lexer has none. | ||
| 62 | +- **A capitalised name is a type** (struct, union, variant, augmentation target). **`Some`, `None`, `Ok` and `Err` are types, not constants** — in Golo they are variants of ordinary unions from `gololang.Errors`, available only after an import, and colouring them as built in would say otherwise. The name after `function` is a function by position; the dotted path after `module` or `import` is one type span. | ||
| 63 | +- **Names use the lexer's own predicate**, not turbo-core's ASCII one: any Unicode letter or mark, `_`, and four emoji blocks. `let 😀 = 1` and `function 🚀launch = …` are coloured, and so are `été` and `名前`. | ||
| 64 | +- **The builtins table is held to the real `golo lsp`** by a test — the 157 names `evaluator.BuiltinNames()` lists minus the five `__`-prefixed test counters — rather than remembered. 38 keywords, 3 constants. | ||
| 65 | +- **The scanner colours what the lexer reads, and the parser (v0.1.1) refuses some of it**: `42L`, `3.14F`, `'c'`, `..`, `orIfNull`, `oftype`, `local function`. A coloured token is not a promise the interpreter accepts it; `reference/languages.md` says so and `demos/syntax-tour/lexer-only.golo` holds them. | ||
| 66 | +- **There is no `golo fmt` and no `golo lint`.** The lints live in the language server, so the starter tools file has no Format or Lint entry — Run comes first, because for a scripting language "what does it print?" is the question asked most often. | ||
| 67 | +- **Eight tools in the Golo menu plus `Echo` in a `Tools` menu**: Run, Test, Test one, Debug, REPL, New script, Build native (`gogolo`), Build wasm (`wagolo`). **Six use a `{{placeholder}}`**, more than any sibling, because with no manifest every command that touches a file has to be told which one. All commands were run against the real binaries. | ||
| 68 | +- **Snippet bodies are TOML literal strings, indented two spaces.** Golo strings carry `\n` and `\"`, which TOML basic strings would resolve; two spaces is the convention in every GoloScript example, and Golo has no formatter to disagree with. | ||
| 69 | +- **`autosave = true` in the starter settings file, `false` in `settings.Default()`.** Two statements in two places on purpose. | ||
| 70 | +- **The starter templates are embedded files, not Go constants**, with the `.tmpl` suffix because `settings.toml.tmpl` holds `theme = %q`, which is not valid TOML. | ||
| 71 | +- **A shebang naming `golo` is claimed** (`#!/usr/bin/env golo`): `#` opens a comment in Golo, so the interpreter reads the line as one. | ||
| 72 | +- **Agent windows are turbo-core's, and what belongs here is the starter file.** `acp.toml.tmpl` is the fourth embedded template, and `profile.Templates.Agents` is the whole of Turbo Golo's contribution to the feature. The example agent is `docker agent serve acp .turbo-golo/agent.yaml`; the only other thing about this editor in it is the sentence saying a ```golo fence is coloured by the scanner this editor colours its own files with. Every other editor got agent windows the same way — one file, one line. The reasoning, and why it could not have been built here, is in `docs/*/explanation/agent-windows.md`. | ||
| 73 | +- **The starter agents file teaches the window's keyboard as well as the format.** `Enter`, `Alt-Enter`, `Tab`, `Esc`, `Ctrl-W` and the copying keys are all in its comments, because a file the editor hands you is the one document a user is guaranteed to see. | ||
| 74 | + | ||
| 75 | +## State as of 2026-09-14 | ||
| 76 | + | ||
| 77 | +- **Complete and green, uncommitted.** `go build`, `go vet`, `gofmt -l` clean; **250 tests pass, 0 skipped** with `golo` v0.1.1 installed. The repository is at its initial commit `7069d2c` with everything untracked. | ||
| 78 | +- **Quality gate: PASS**, run #2 (run #1 failed on 2 smells, fixed the same minute). 0 errors, 0 warnings, 0 smells; total complexity 54, worst file `words.go` at 29 against a limit of 60; 1002 lines, 491 of code. | ||
| 79 | +- **Documentation**: 34 pages × EN + FR, a `README.md` at the root, `demos/README.md`, and `docs/diagrams/packages.drawio` checked against `go list`. The French tutorial was the last page written, on the second session of the day. | ||
| 80 | +- **Verified in a real pty** (`/tmp/tg/tut.raw`, replayed with `/tmp/tg/screen.py`): the bar ` File Edit Search Run Code Options Window Snippets Golo Help`; the `Tools` menu appearing between Golo and Help once the tools file exists; `Run` asking `script, e.g. main.golo:` then a terminal window printing the three greetings; **a `// run it` line showing `×` (`91;44;1`) in the gutter and `⚠ GoloScript has no '//' line comments…` on the status bar**; the Options menu. Tutorial colours read back as SGR: keyword `97;44;1`, type `96;44`, function `93;44;1`, builtin `96;44;1`, identifier `93;44`, string `92;44`, comment `38;2;143;143;143`, operator `37;44`; numbers `95;44`. From File, `→` five times reaches Options and eight reaches Golo — read off the wire. | ||
| 81 | +- **Registered in the family.** turbo-core's `README.md`, both doc READMEs, both workspace how-tos, `profile/profile.go`'s comment and `.memory/summary.md` count five editors; turbo-python's and turbo-moonbit's architecture pages say "all five editors". turbo-core's `.go` strings were swept for hardcoded language names against v0.5.0: every hit innocent, **no library change needed** — Turbo Golo is the first editor with no root marker and the library's fallback handled it unchanged. | ||
| 82 | + | ||
| 83 | +## Known boundaries | ||
| 84 | + | ||
| 85 | +- **Definition and document symbols are file-local**: top-level functions and unions, variants nested under their union. Imports of *user* modules on disk are not resolved by the server. | ||
| 86 | +- **A diagnostic without a line number lands on line 1**, and the whole line is marked — the server gives no column. | ||
| 87 | +- **`typeDefinition` reports nothing found**, because the server does not provide it; `references`, `implementation` and `workspace/symbol` are answered since GoloScript v0.2.0. | ||
| 88 | + | ||
| 89 | +## Not yet established | ||
| 90 | + | ||
| 91 | +- **Agent windows have never been opened in this editor.** The feature is turbo-core's and was driven end to end from turbo-go against a real `docker agent` and a real llama.cpp; what is here is the starter file, covered by tests that create it, load it back and check it names this editor's own language. Nobody has run `turbo-golo`, pressed `Alt-A` and talked to an agent from it. | ||
| 92 | + | ||
| 93 | + | ||
| 94 | +- **No record that the tests were falsified by mutation.** The session that wrote them ended before writing this file, and left only `/tmp/tg/FACTS.md` behind. Every sibling's first session ran a mutation pass; whether this one did is unknown. Running one is the next thing worth doing before a release. | ||
| 95 | +- **Never run on macOS or Windows.** Everything here was verified on Linux/arm64. A binary built in this sandbox is an ELF. | ||
| 96 | +- **No CI.** There is no pipeline configuration in the repository. | ||
| 97 | +- **Never released.** No tag exists; `release.env` says `TAG="v0.1.0"`, which is right for a first tag. `01`–`04` have only ever been read here, never run. | ||
| 98 | +- **Performance on a large file is unmeasured**, and a string carried across many lines re-scans on every edit like any carry does. | ||
| 99 | +- **Nothing checks the demos automatically.** They were run by hand under `golo` v0.1.1; a future GoloScript could change what the parser accepts without anything here noticing. | ||
| 100 | +- **The `golo lsp` tests have only been run against v0.1.1.** The builtins table is held to that version's `BuiltinNames()`; a newer golo that adds a builtin will fail the test, which is the intent. | ||
| 101 | + | ||
| 102 | +## State as of 2026-09-19 — moved to Rickub, released by a workflow | ||
| 103 | + | ||
| 104 | +- **Module path `rickub.com/turbo-editors/turbo-golo`**, depending on `rickub.com/turbo-editors/turbo-core v1.0.0` — the first turbo-core version published under that path (`v0.9.0` on the proxy still declares the Codeberg path and cannot be required as `rickub.com/…`). Every import, the Makefile's `VERSION_PKG`, `scripts/install.sh`, the README and the docs say `rickub.com`. `GOWORK=off make check` green. The repository on this side is a fresh `git init` with `origin` at `ssh://git@rickub.com/turbo-editors/turbo-golo.git` and **no commit yet**; `01-release.tag.sh` makes the first one. | ||
| 105 | +- **Releases are one script and one workflow**, modelled on turbo-core's and identical to turbo-go's. `01-release.tag.sh` runs `make check` under `TURBO_GOLO_RELEASING=1`, refuses a tag taken locally or on origin (bump, never move), refuses a `replace` in `go.mod`, commits, pushes the current branch, then tags and pushes the tag. That push starts `.github/workflows/release.yml`: `go test` with `TURBO_GOLO_RELEASING=1`, `./02-build-releases.sh "${GITHUB_REF_NAME}"`, release notes from the tag message, a run artifact, then `softprops/action-gh-release@v2` attaching `turbo-golo-*`, `SHA256SUMS` and `README.md` with the job's own `GITHUB_TOKEN` — the only credential Rickub's release API accepts. **`02-release.publish.sh` and `04-release.upload-binaries.sh` are gone**; the build script is now `02-build-releases.sh`, takes the tag as `$1` (CI has no `release.env`), validates it, refuses a `replace`, and starts from an empty `release/${TAG}/`. `release.env` holds only `TAG` and `ABOUT`; `turbo-golo.token.env` is read by nothing. | ||
| 106 | +- **`release_test.go`** runs `01` for real against a throwaway bare remote (publishes, then refuses the same tag), runs `02` alone to see it refuse `v0.o.0`, and asserts the workflow's trigger, `contents: write`, `./02-build-releases.sh`, `fail_on_unmatched_files`, docs linked at the tag, no `secrets.`, and `TURBO_GOLO_RELEASING`. The copy of the module for the clone leaves out `.git`, `bin`, `release`, `kits`, `demo`, `demos`, `*.env` and `go.work*`; children run with `GOWORK=off`. | ||
added
.qlty/.gitignore +7 -0 | new file mode 100644 | ||
| @@ -0,0 +1,7 @@ | ||
| 1 | +* | |
| 2 | +!configs | |
| 3 | +!configs/** | |
| 4 | +!hooks | |
| 5 | +!hooks/** | |
| 6 | +!qlty.toml | |
| 7 | +!.gitignore | |
| new file mode 100644 | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | +* | ||
| 2 | +!configs | ||
| 3 | +!configs/** | ||
| 4 | +!hooks | ||
| 5 | +!hooks/** | ||
| 6 | +!qlty.toml | ||
| 7 | +!.gitignore | ||
added
.qlty/qlty.toml +65 -0 | new file mode 100644 | ||
| @@ -0,0 +1,65 @@ | ||
| 1 | +# This file was automatically generated by `qlty init`. | |
| 2 | +# You can modify it to suit your needs. | |
| 3 | +# We recommend you to commit this file to your repository. | |
| 4 | +# | |
| 5 | +# This configuration is used by both Qlty CLI and Qlty Cloud. | |
| 6 | +# | |
| 7 | +# Qlty CLI -- Code quality toolkit for developers | |
| 8 | +# Qlty Cloud -- Fully automated Code Health Platform | |
| 9 | +# | |
| 10 | +# Try Qlty Cloud: https://qlty.sh | |
| 11 | +# | |
| 12 | +# For a guide to configuration, visit https://qlty.sh/d/config | |
| 13 | +# Or for a full reference, visit https://qlty.sh/d/qlty-toml | |
| 14 | +config_version = "0" | |
| 15 | + | |
| 16 | +exclude_patterns = [ | |
| 17 | + "*_min.*", | |
| 18 | + "*-min.*", | |
| 19 | + "*.min.*", | |
| 20 | + "**/.yarn/**", | |
| 21 | + "**/*.d.ts", | |
| 22 | + "**/assets/**", | |
| 23 | + "**/bower_components/**", | |
| 24 | + "**/build/**", | |
| 25 | + "**/cache/**", | |
| 26 | + "**/config/**", | |
| 27 | + "**/db/**", | |
| 28 | + "**/deps/**", | |
| 29 | + "**/dist/**", | |
| 30 | + "**/extern/**", | |
| 31 | + "**/external/**", | |
| 32 | + "**/generated/**", | |
| 33 | + "**/Godeps/**", | |
| 34 | + "**/gradlew/**", | |
| 35 | + "**/mvnw/**", | |
| 36 | + "**/node_modules/**", | |
| 37 | + "**/protos/**", | |
| 38 | + "**/seed/**", | |
| 39 | + "**/target/**", | |
| 40 | + "**/templates/**", | |
| 41 | + "**/testdata/**", | |
| 42 | + "**/vendor/**", | |
| 43 | +] | |
| 44 | + | |
| 45 | +test_patterns = [ | |
| 46 | + "**/test/**", | |
| 47 | + "**/spec/**", | |
| 48 | + "**/*.test.*", | |
| 49 | + "**/*.spec.*", | |
| 50 | + "**/*_test.*", | |
| 51 | + "**/*_spec.*", | |
| 52 | + "**/test_*.*", | |
| 53 | + "**/spec_*.*", | |
| 54 | +] | |
| 55 | + | |
| 56 | +[smells] | |
| 57 | +mode = "comment" | |
| 58 | + | |
| 59 | +[[source]] | |
| 60 | +name = "default" | |
| 61 | +default = true | |
| 62 | + | |
| 63 | + | |
| 64 | +[[plugin]] | |
| 65 | +name = "trufflehog" | |
| new file mode 100644 | |||
| @@ -0,0 +1,65 @@ | |||
| 1 | +# This file was automatically generated by `qlty init`. | ||
| 2 | +# You can modify it to suit your needs. | ||
| 3 | +# We recommend you to commit this file to your repository. | ||
| 4 | +# | ||
| 5 | +# This configuration is used by both Qlty CLI and Qlty Cloud. | ||
| 6 | +# | ||
| 7 | +# Qlty CLI -- Code quality toolkit for developers | ||
| 8 | +# Qlty Cloud -- Fully automated Code Health Platform | ||
| 9 | +# | ||
| 10 | +# Try Qlty Cloud: https://qlty.sh | ||
| 11 | +# | ||
| 12 | +# For a guide to configuration, visit https://qlty.sh/d/config | ||
| 13 | +# Or for a full reference, visit https://qlty.sh/d/qlty-toml | ||
| 14 | +config_version = "0" | ||
| 15 | + | ||
| 16 | +exclude_patterns = [ | ||
| 17 | + "*_min.*", | ||
| 18 | + "*-min.*", | ||
| 19 | + "*.min.*", | ||
| 20 | + "**/.yarn/**", | ||
| 21 | + "**/*.d.ts", | ||
| 22 | + "**/assets/**", | ||
| 23 | + "**/bower_components/**", | ||
| 24 | + "**/build/**", | ||
| 25 | + "**/cache/**", | ||
| 26 | + "**/config/**", | ||
| 27 | + "**/db/**", | ||
| 28 | + "**/deps/**", | ||
| 29 | + "**/dist/**", | ||
| 30 | + "**/extern/**", | ||
| 31 | + "**/external/**", | ||
| 32 | + "**/generated/**", | ||
| 33 | + "**/Godeps/**", | ||
| 34 | + "**/gradlew/**", | ||
| 35 | + "**/mvnw/**", | ||
| 36 | + "**/node_modules/**", | ||
| 37 | + "**/protos/**", | ||
| 38 | + "**/seed/**", | ||
| 39 | + "**/target/**", | ||
| 40 | + "**/templates/**", | ||
| 41 | + "**/testdata/**", | ||
| 42 | + "**/vendor/**", | ||
| 43 | +] | ||
| 44 | + | ||
| 45 | +test_patterns = [ | ||
| 46 | + "**/test/**", | ||
| 47 | + "**/spec/**", | ||
| 48 | + "**/*.test.*", | ||
| 49 | + "**/*.spec.*", | ||
| 50 | + "**/*_test.*", | ||
| 51 | + "**/*_spec.*", | ||
| 52 | + "**/test_*.*", | ||
| 53 | + "**/spec_*.*", | ||
| 54 | +] | ||
| 55 | + | ||
| 56 | +[smells] | ||
| 57 | +mode = "comment" | ||
| 58 | + | ||
| 59 | +[[source]] | ||
| 60 | +name = "default" | ||
| 61 | +default = true | ||
| 62 | + | ||
| 63 | + | ||
| 64 | +[[plugin]] | ||
| 65 | +name = "trufflehog" | ||
added
.quality/history.jsonl +4 -0 | new file mode 100644 | ||
| @@ -0,0 +1,4 @@ | ||
| 1 | +{"branch": "main", "breaches": ["code smells: 2 (max 0)"], "commit": "7069d2c", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 2, "complex": 53, "cyclo": 131, "fields": 5, "funcs": 41, "lcom": 0, "lines": 991, "loc": 483}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 1, "smells": 2, "timestamp": "2026-09-14T04:44:06Z"} | |
| 2 | +{"branch": "main", "breaches": [], "commit": "7069d2c", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 54, "cyclo": 121, "fields": 5, "funcs": 41, "lcom": 0, "lines": 1002, "loc": 491}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 2, "smells": 0, "timestamp": "2026-09-14T04:44:49Z"} | |
| 3 | +{"branch": "main", "breaches": [], "commit": "7069d2c", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 54, "cyclo": 121, "fields": 5, "funcs": 41, "lcom": 0, "lines": 1002, "loc": 491}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 3, "smells": 0, "timestamp": "2026-09-14T05:30:28Z"} | |
| 4 | +{"branch": "feature/acp", "breaches": [], "commit": "8df88bf", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 54, "cyclo": 121, "fields": 5, "funcs": 41, "lcom": 0, "lines": 1012, "loc": 493}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 4, "smells": 0, "timestamp": "2026-09-15T16:54:06Z"} | |
| new file mode 100644 | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | +{"branch": "main", "breaches": ["code smells: 2 (max 0)"], "commit": "7069d2c", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 2, "complex": 53, "cyclo": 131, "fields": 5, "funcs": 41, "lcom": 0, "lines": 991, "loc": 483}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 1, "smells": 2, "timestamp": "2026-09-14T04:44:06Z"} | ||
| 2 | +{"branch": "main", "breaches": [], "commit": "7069d2c", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 54, "cyclo": 121, "fields": 5, "funcs": 41, "lcom": 0, "lines": 1002, "loc": 491}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 2, "smells": 0, "timestamp": "2026-09-14T04:44:49Z"} | ||
| 3 | +{"branch": "main", "breaches": [], "commit": "7069d2c", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 54, "cyclo": 121, "fields": 5, "funcs": 41, "lcom": 0, "lines": 1002, "loc": 491}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 3, "smells": 0, "timestamp": "2026-09-14T05:30:28Z"} | ||
| 4 | +{"branch": "feature/acp", "breaches": [], "commit": "8df88bf", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 54, "cyclo": 121, "fields": 5, "funcs": 41, "lcom": 0, "lines": 1012, "loc": 493}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 4, "smells": 0, "timestamp": "2026-09-15T16:54:06Z"} | ||
added
.quality/report-20260914T044406Z.md +61 -0 | new file mode 100644 | ||
| @@ -0,0 +1,61 @@ | ||
| 1 | +# Quality report — 2026-09-14T04:44:06Z | |
| 2 | + | |
| 3 | +- **Gate**: ❌ **FAIL** | |
| 4 | +- **Commit**: `7069d2c` on `main` | |
| 5 | +- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23) | |
| 6 | +- **Run**: #1 (first recorded run) | |
| 7 | + | |
| 8 | +## Gate violations | |
| 9 | + | |
| 10 | +- code smells: 2 (max 0) | |
| 11 | + | |
| 12 | +## Lint issues (`qlty check`) | |
| 13 | + | |
| 14 | +_none_ | |
| 15 | + | |
| 16 | +### Top rules | |
| 17 | + | |
| 18 | +_none_ | |
| 19 | + | |
| 20 | +### Most affected files | |
| 21 | + | |
| 22 | +_none_ | |
| 23 | + | |
| 24 | +## Code smells (`qlty smells`) | |
| 25 | + | |
| 26 | +Total: **2** (vs previous: —) | |
| 27 | + | |
| 28 | +| smell | file | line | detail | | |
| 29 | +|---|---|---|---| | |
| 30 | +| qlty:boolean-logic | internal/gololang/words.go | 105 | Complex binary expression | | |
| 31 | +| qlty:boolean-logic | internal/gololang/words.go | 106 | Complex binary expression | | |
| 32 | + | |
| 33 | +## Metrics (`qlty metrics`) | |
| 34 | + | |
| 35 | +| metric | total | vs previous | | |
| 36 | +|---|---|---| | |
| 37 | +| funcs | 41 | — | | |
| 38 | +| classes | 2 | — | | |
| 39 | +| fields | 5 | — | | |
| 40 | +| cyclo | 131 | — | | |
| 41 | +| complex | 53 | — | | |
| 42 | +| lcom | 0 | — | | |
| 43 | +| lines | 991 | — | | |
| 44 | +| loc | 483 | — | | |
| 45 | + | |
| 46 | +### Most complex files | |
| 47 | + | |
| 48 | +| file | complex | cyclo | loc | | |
| 49 | +|---|---|---|---| | |
| 50 | +| internal/gololang/words.go | 28 | 63 | 185 | | |
| 51 | +| main.go | 16 | 32 | 132 | | |
| 52 | +| internal/gololang/scan.go | 6 | 30 | 81 | | |
| 53 | +| internal/gololang/literals.go | 3 | 4 | 33 | | |
| 54 | +| internal/gololang/gololang.go | 0 | 1 | 47 | | |
| 55 | +| internal/gololang/templates.go | 0 | 1 | 5 | | |
| 56 | + | |
| 57 | +## Trend | |
| 58 | + | |
| 59 | +| run | timestamp | error | warning | smells | complex | gate | | |
| 60 | +|---|---|---|---|---|---|---| | |
| 61 | +| 1 | 2026-09-14T04:44:06Z | 0 | 0 | 2 | 53 | FAIL | | |
| new file mode 100644 | |||
| @@ -0,0 +1,61 @@ | |||
| 1 | +# Quality report — 2026-09-14T04:44:06Z | ||
| 2 | + | ||
| 3 | +- **Gate**: ❌ **FAIL** | ||
| 4 | +- **Commit**: `7069d2c` on `main` | ||
| 5 | +- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23) | ||
| 6 | +- **Run**: #1 (first recorded run) | ||
| 7 | + | ||
| 8 | +## Gate violations | ||
| 9 | + | ||
| 10 | +- code smells: 2 (max 0) | ||
| 11 | + | ||
| 12 | +## Lint issues (`qlty check`) | ||
| 13 | + | ||
| 14 | +_none_ | ||
| 15 | + | ||
| 16 | +### Top rules | ||
| 17 | + | ||
| 18 | +_none_ | ||
| 19 | + | ||
| 20 | +### Most affected files | ||
| 21 | + | ||
| 22 | +_none_ | ||
| 23 | + | ||
| 24 | +## Code smells (`qlty smells`) | ||
| 25 | + | ||
| 26 | +Total: **2** (vs previous: —) | ||
| 27 | + | ||
| 28 | +| smell | file | line | detail | | ||
| 29 | +|---|---|---|---| | ||
| 30 | +| qlty:boolean-logic | internal/gololang/words.go | 105 | Complex binary expression | | ||
| 31 | +| qlty:boolean-logic | internal/gololang/words.go | 106 | Complex binary expression | | ||
| 32 | + | ||
| 33 | +## Metrics (`qlty metrics`) | ||
| 34 | + | ||
| 35 | +| metric | total | vs previous | | ||
| 36 | +|---|---|---| | ||
| 37 | +| funcs | 41 | — | | ||
| 38 | +| classes | 2 | — | | ||
| 39 | +| fields | 5 | — | | ||
| 40 | +| cyclo | 131 | — | | ||
| 41 | +| complex | 53 | — | | ||
| 42 | +| lcom | 0 | — | | ||
| 43 | +| lines | 991 | — | | ||
| 44 | +| loc | 483 | — | | ||
| 45 | + | ||
| 46 | +### Most complex files | ||
| 47 | + | ||
| 48 | +| file | complex | cyclo | loc | | ||
| 49 | +|---|---|---|---| | ||
| 50 | +| internal/gololang/words.go | 28 | 63 | 185 | | ||
| 51 | +| main.go | 16 | 32 | 132 | | ||
| 52 | +| internal/gololang/scan.go | 6 | 30 | 81 | | ||
| 53 | +| internal/gololang/literals.go | 3 | 4 | 33 | | ||
| 54 | +| internal/gololang/gololang.go | 0 | 1 | 47 | | ||
| 55 | +| internal/gololang/templates.go | 0 | 1 | 5 | | ||
| 56 | + | ||
| 57 | +## Trend | ||
| 58 | + | ||
| 59 | +| run | timestamp | error | warning | smells | complex | gate | | ||
| 60 | +|---|---|---|---|---|---|---| | ||
| 61 | +| 1 | 2026-09-14T04:44:06Z | 0 | 0 | 2 | 53 | FAIL | | ||
added
.quality/report-20260914T044449Z.md +55 -0 | new file mode 100644 | ||
| @@ -0,0 +1,55 @@ | ||
| 1 | +# Quality report — 2026-09-14T04:44:49Z | |
| 2 | + | |
| 3 | +- **Gate**: ✅ **PASS** | |
| 4 | +- **Commit**: `7069d2c` on `main` | |
| 5 | +- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23) | |
| 6 | +- **Run**: #2 (previous: 2026-09-14T04:44:06Z) | |
| 7 | + | |
| 8 | +## Lint issues (`qlty check`) | |
| 9 | + | |
| 10 | +_none_ | |
| 11 | + | |
| 12 | +### Top rules | |
| 13 | + | |
| 14 | +_none_ | |
| 15 | + | |
| 16 | +### Most affected files | |
| 17 | + | |
| 18 | +_none_ | |
| 19 | + | |
| 20 | +## Code smells (`qlty smells`) | |
| 21 | + | |
| 22 | +Total: **0** (vs previous: -2) | |
| 23 | + | |
| 24 | +_none_ | |
| 25 | + | |
| 26 | +## Metrics (`qlty metrics`) | |
| 27 | + | |
| 28 | +| metric | total | vs previous | | |
| 29 | +|---|---|---| | |
| 30 | +| funcs | 41 | ±0 | | |
| 31 | +| classes | 2 | ±0 | | |
| 32 | +| fields | 5 | ±0 | | |
| 33 | +| cyclo | 121 | -10 | | |
| 34 | +| complex | 54 | +1 | | |
| 35 | +| lcom | 0 | ±0 | | |
| 36 | +| lines | 1002 | +11 | | |
| 37 | +| loc | 491 | +8 | | |
| 38 | + | |
| 39 | +### Most complex files | |
| 40 | + | |
| 41 | +| file | complex | cyclo | loc | | |
| 42 | +|---|---|---|---| | |
| 43 | +| internal/gololang/words.go | 29 | 53 | 193 | | |
| 44 | +| main.go | 16 | 32 | 132 | | |
| 45 | +| internal/gololang/scan.go | 6 | 30 | 81 | | |
| 46 | +| internal/gololang/literals.go | 3 | 4 | 33 | | |
| 47 | +| internal/gololang/gololang.go | 0 | 1 | 47 | | |
| 48 | +| internal/gololang/templates.go | 0 | 1 | 5 | | |
| 49 | + | |
| 50 | +## Trend | |
| 51 | + | |
| 52 | +| run | timestamp | error | warning | smells | complex | gate | | |
| 53 | +|---|---|---|---|---|---|---| | |
| 54 | +| 1 | 2026-09-14T04:44:06Z | 0 | 0 | 2 | 53 | FAIL | | |
| 55 | +| 2 | 2026-09-14T04:44:49Z | 0 | 0 | 0 | 54 | PASS | | |
| new file mode 100644 | |||
| @@ -0,0 +1,55 @@ | |||
| 1 | +# Quality report — 2026-09-14T04:44:49Z | ||
| 2 | + | ||
| 3 | +- **Gate**: ✅ **PASS** | ||
| 4 | +- **Commit**: `7069d2c` on `main` | ||
| 5 | +- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23) | ||
| 6 | +- **Run**: #2 (previous: 2026-09-14T04:44:06Z) | ||
| 7 | + | ||
| 8 | +## Lint issues (`qlty check`) | ||
| 9 | + | ||
| 10 | +_none_ | ||
| 11 | + | ||
| 12 | +### Top rules | ||
| 13 | + | ||
| 14 | +_none_ | ||
| 15 | + | ||
| 16 | +### Most affected files | ||
| 17 | + | ||
| 18 | +_none_ | ||
| 19 | + | ||
| 20 | +## Code smells (`qlty smells`) | ||
| 21 | + | ||
| 22 | +Total: **0** (vs previous: -2) | ||
| 23 | + | ||
| 24 | +_none_ | ||
| 25 | + | ||
| 26 | +## Metrics (`qlty metrics`) | ||
| 27 | + | ||
| 28 | +| metric | total | vs previous | | ||
| 29 | +|---|---|---| | ||
| 30 | +| funcs | 41 | ±0 | | ||
| 31 | +| classes | 2 | ±0 | | ||
| 32 | +| fields | 5 | ±0 | | ||
| 33 | +| cyclo | 121 | -10 | | ||
| 34 | +| complex | 54 | +1 | | ||
| 35 | +| lcom | 0 | ±0 | | ||
| 36 | +| lines | 1002 | +11 | | ||
| 37 | +| loc | 491 | +8 | | ||
| 38 | + | ||
| 39 | +### Most complex files | ||
| 40 | + | ||
| 41 | +| file | complex | cyclo | loc | | ||
| 42 | +|---|---|---|---| | ||
| 43 | +| internal/gololang/words.go | 29 | 53 | 193 | | ||
| 44 | +| main.go | 16 | 32 | 132 | | ||
| 45 | +| internal/gololang/scan.go | 6 | 30 | 81 | | ||
| 46 | +| internal/gololang/literals.go | 3 | 4 | 33 | | ||
| 47 | +| internal/gololang/gololang.go | 0 | 1 | 47 | | ||
| 48 | +| internal/gololang/templates.go | 0 | 1 | 5 | | ||
| 49 | + | ||
| 50 | +## Trend | ||
| 51 | + | ||
| 52 | +| run | timestamp | error | warning | smells | complex | gate | | ||
| 53 | +|---|---|---|---|---|---|---| | ||
| 54 | +| 1 | 2026-09-14T04:44:06Z | 0 | 0 | 2 | 53 | FAIL | | ||
| 55 | +| 2 | 2026-09-14T04:44:49Z | 0 | 0 | 0 | 54 | PASS | | ||
added
.quality/report-20260914T053028Z.md +56 -0 | new file mode 100644 | ||
| @@ -0,0 +1,56 @@ | ||
| 1 | +# Quality report — 2026-09-14T05:30:28Z | |
| 2 | + | |
| 3 | +- **Gate**: ✅ **PASS** | |
| 4 | +- **Commit**: `7069d2c` on `main` | |
| 5 | +- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23) | |
| 6 | +- **Run**: #3 (previous: 2026-09-14T04:44:49Z) | |
| 7 | + | |
| 8 | +## Lint issues (`qlty check`) | |
| 9 | + | |
| 10 | +_none_ | |
| 11 | + | |
| 12 | +### Top rules | |
| 13 | + | |
| 14 | +_none_ | |
| 15 | + | |
| 16 | +### Most affected files | |
| 17 | + | |
| 18 | +_none_ | |
| 19 | + | |
| 20 | +## Code smells (`qlty smells`) | |
| 21 | + | |
| 22 | +Total: **0** (vs previous: ±0) | |
| 23 | + | |
| 24 | +_none_ | |
| 25 | + | |
| 26 | +## Metrics (`qlty metrics`) | |
| 27 | + | |
| 28 | +| metric | total | vs previous | | |
| 29 | +|---|---|---| | |
| 30 | +| funcs | 41 | ±0 | | |
| 31 | +| classes | 2 | ±0 | | |
| 32 | +| fields | 5 | ±0 | | |
| 33 | +| cyclo | 121 | ±0 | | |
| 34 | +| complex | 54 | ±0 | | |
| 35 | +| lcom | 0 | ±0 | | |
| 36 | +| lines | 1002 | ±0 | | |
| 37 | +| loc | 491 | ±0 | | |
| 38 | + | |
| 39 | +### Most complex files | |
| 40 | + | |
| 41 | +| file | complex | cyclo | loc | | |
| 42 | +|---|---|---|---| | |
| 43 | +| internal/gololang/words.go | 29 | 53 | 193 | | |
| 44 | +| main.go | 16 | 32 | 132 | | |
| 45 | +| internal/gololang/scan.go | 6 | 30 | 81 | | |
| 46 | +| internal/gololang/literals.go | 3 | 4 | 33 | | |
| 47 | +| internal/gololang/gololang.go | 0 | 1 | 47 | | |
| 48 | +| internal/gololang/templates.go | 0 | 1 | 5 | | |
| 49 | + | |
| 50 | +## Trend | |
| 51 | + | |
| 52 | +| run | timestamp | error | warning | smells | complex | gate | | |
| 53 | +|---|---|---|---|---|---|---| | |
| 54 | +| 1 | 2026-09-14T04:44:06Z | 0 | 0 | 2 | 53 | FAIL | | |
| 55 | +| 2 | 2026-09-14T04:44:49Z | 0 | 0 | 0 | 54 | PASS | | |
| 56 | +| 3 | 2026-09-14T05:30:28Z | 0 | 0 | 0 | 54 | PASS | | |
| new file mode 100644 | |||
| @@ -0,0 +1,56 @@ | |||
| 1 | +# Quality report — 2026-09-14T05:30:28Z | ||
| 2 | + | ||
| 3 | +- **Gate**: ✅ **PASS** | ||
| 4 | +- **Commit**: `7069d2c` on `main` | ||
| 5 | +- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23) | ||
| 6 | +- **Run**: #3 (previous: 2026-09-14T04:44:49Z) | ||
| 7 | + | ||
| 8 | +## Lint issues (`qlty check`) | ||
| 9 | + | ||
| 10 | +_none_ | ||
| 11 | + | ||
| 12 | +### Top rules | ||
| 13 | + | ||
| 14 | +_none_ | ||
| 15 | + | ||
| 16 | +### Most affected files | ||
| 17 | + | ||
| 18 | +_none_ | ||
| 19 | + | ||
| 20 | +## Code smells (`qlty smells`) | ||
| 21 | + | ||
| 22 | +Total: **0** (vs previous: ±0) | ||
| 23 | + | ||
| 24 | +_none_ | ||
| 25 | + | ||
| 26 | +## Metrics (`qlty metrics`) | ||
| 27 | + | ||
| 28 | +| metric | total | vs previous | | ||
| 29 | +|---|---|---| | ||
| 30 | +| funcs | 41 | ±0 | | ||
| 31 | +| classes | 2 | ±0 | | ||
| 32 | +| fields | 5 | ±0 | | ||
| 33 | +| cyclo | 121 | ±0 | | ||
| 34 | +| complex | 54 | ±0 | | ||
| 35 | +| lcom | 0 | ±0 | | ||
| 36 | +| lines | 1002 | ±0 | | ||
| 37 | +| loc | 491 | ±0 | | ||
| 38 | + | ||
| 39 | +### Most complex files | ||
| 40 | + | ||
| 41 | +| file | complex | cyclo | loc | | ||
| 42 | +|---|---|---|---| | ||
| 43 | +| internal/gololang/words.go | 29 | 53 | 193 | | ||
| 44 | +| main.go | 16 | 32 | 132 | | ||
| 45 | +| internal/gololang/scan.go | 6 | 30 | 81 | | ||
| 46 | +| internal/gololang/literals.go | 3 | 4 | 33 | | ||
| 47 | +| internal/gololang/gololang.go | 0 | 1 | 47 | | ||
| 48 | +| internal/gololang/templates.go | 0 | 1 | 5 | | ||
| 49 | + | ||
| 50 | +## Trend | ||
| 51 | + | ||
| 52 | +| run | timestamp | error | warning | smells | complex | gate | | ||
| 53 | +|---|---|---|---|---|---|---| | ||
| 54 | +| 1 | 2026-09-14T04:44:06Z | 0 | 0 | 2 | 53 | FAIL | | ||
| 55 | +| 2 | 2026-09-14T04:44:49Z | 0 | 0 | 0 | 54 | PASS | | ||
| 56 | +| 3 | 2026-09-14T05:30:28Z | 0 | 0 | 0 | 54 | PASS | | ||
added
.quality/report-20260915T165406Z.md +57 -0 | new file mode 100644 | ||
| @@ -0,0 +1,57 @@ | ||
| 1 | +# Quality report — 2026-09-15T16:54:06Z | |
| 2 | + | |
| 3 | +- **Gate**: ✅ **PASS** | |
| 4 | +- **Commit**: `8df88bf` on `feature/acp` | |
| 5 | +- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23) | |
| 6 | +- **Run**: #4 (previous: 2026-09-14T05:30:28Z) | |
| 7 | + | |
| 8 | +## Lint issues (`qlty check`) | |
| 9 | + | |
| 10 | +_none_ | |
| 11 | + | |
| 12 | +### Top rules | |
| 13 | + | |
| 14 | +_none_ | |
| 15 | + | |
| 16 | +### Most affected files | |
| 17 | + | |
| 18 | +_none_ | |
| 19 | + | |
| 20 | +## Code smells (`qlty smells`) | |
| 21 | + | |
| 22 | +Total: **0** (vs previous: ±0) | |
| 23 | + | |
| 24 | +_none_ | |
| 25 | + | |
| 26 | +## Metrics (`qlty metrics`) | |
| 27 | + | |
| 28 | +| metric | total | vs previous | | |
| 29 | +|---|---|---| | |
| 30 | +| funcs | 41 | ±0 | | |
| 31 | +| classes | 2 | ±0 | | |
| 32 | +| fields | 5 | ±0 | | |
| 33 | +| cyclo | 121 | ±0 | | |
| 34 | +| complex | 54 | ±0 | | |
| 35 | +| lcom | 0 | ±0 | | |
| 36 | +| lines | 1012 | +10 | | |
| 37 | +| loc | 493 | +2 | | |
| 38 | + | |
| 39 | +### Most complex files | |
| 40 | + | |
| 41 | +| file | complex | cyclo | loc | | |
| 42 | +|---|---|---|---| | |
| 43 | +| internal/gololang/words.go | 29 | 53 | 193 | | |
| 44 | +| main.go | 16 | 32 | 132 | | |
| 45 | +| internal/gololang/scan.go | 6 | 30 | 81 | | |
| 46 | +| internal/gololang/literals.go | 3 | 4 | 33 | | |
| 47 | +| internal/gololang/gololang.go | 0 | 1 | 48 | | |
| 48 | +| internal/gololang/templates.go | 0 | 1 | 6 | | |
| 49 | + | |
| 50 | +## Trend | |
| 51 | + | |
| 52 | +| run | timestamp | error | warning | smells | complex | gate | | |
| 53 | +|---|---|---|---|---|---|---| | |
| 54 | +| 1 | 2026-09-14T04:44:06Z | 0 | 0 | 2 | 53 | FAIL | | |
| 55 | +| 2 | 2026-09-14T04:44:49Z | 0 | 0 | 0 | 54 | PASS | | |
| 56 | +| 3 | 2026-09-14T05:30:28Z | 0 | 0 | 0 | 54 | PASS | | |
| 57 | +| 4 | 2026-09-15T16:54:06Z | 0 | 0 | 0 | 54 | PASS | | |
| new file mode 100644 | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | +# Quality report — 2026-09-15T16:54:06Z | ||
| 2 | + | ||
| 3 | +- **Gate**: ✅ **PASS** | ||
| 4 | +- **Commit**: `8df88bf` on `feature/acp` | ||
| 5 | +- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23) | ||
| 6 | +- **Run**: #4 (previous: 2026-09-14T05:30:28Z) | ||
| 7 | + | ||
| 8 | +## Lint issues (`qlty check`) | ||
| 9 | + | ||
| 10 | +_none_ | ||
| 11 | + | ||
| 12 | +### Top rules | ||
| 13 | + | ||
| 14 | +_none_ | ||
| 15 | + | ||
| 16 | +### Most affected files | ||
| 17 | + | ||
| 18 | +_none_ | ||
| 19 | + | ||
| 20 | +## Code smells (`qlty smells`) | ||
| 21 | + | ||
| 22 | +Total: **0** (vs previous: ±0) | ||
| 23 | + | ||
| 24 | +_none_ | ||
| 25 | + | ||
| 26 | +## Metrics (`qlty metrics`) | ||
| 27 | + | ||
| 28 | +| metric | total | vs previous | | ||
| 29 | +|---|---|---| | ||
| 30 | +| funcs | 41 | ±0 | | ||
| 31 | +| classes | 2 | ±0 | | ||
| 32 | +| fields | 5 | ±0 | | ||
| 33 | +| cyclo | 121 | ±0 | | ||
| 34 | +| complex | 54 | ±0 | | ||
| 35 | +| lcom | 0 | ±0 | | ||
| 36 | +| lines | 1012 | +10 | | ||
| 37 | +| loc | 493 | +2 | | ||
| 38 | + | ||
| 39 | +### Most complex files | ||
| 40 | + | ||
| 41 | +| file | complex | cyclo | loc | | ||
| 42 | +|---|---|---|---| | ||
| 43 | +| internal/gololang/words.go | 29 | 53 | 193 | | ||
| 44 | +| main.go | 16 | 32 | 132 | | ||
| 45 | +| internal/gololang/scan.go | 6 | 30 | 81 | | ||
| 46 | +| internal/gololang/literals.go | 3 | 4 | 33 | | ||
| 47 | +| internal/gololang/gololang.go | 0 | 1 | 48 | | ||
| 48 | +| internal/gololang/templates.go | 0 | 1 | 6 | | ||
| 49 | + | ||
| 50 | +## Trend | ||
| 51 | + | ||
| 52 | +| run | timestamp | error | warning | smells | complex | gate | | ||
| 53 | +|---|---|---|---|---|---|---| | ||
| 54 | +| 1 | 2026-09-14T04:44:06Z | 0 | 0 | 2 | 53 | FAIL | | ||
| 55 | +| 2 | 2026-09-14T04:44:49Z | 0 | 0 | 0 | 54 | PASS | | ||
| 56 | +| 3 | 2026-09-14T05:30:28Z | 0 | 0 | 0 | 54 | PASS | | ||
| 57 | +| 4 | 2026-09-15T16:54:06Z | 0 | 0 | 0 | 54 | PASS | | ||
added
.quality/report-latest.md +57 -0 | new file mode 100644 | ||
| @@ -0,0 +1,57 @@ | ||
| 1 | +# Quality report — 2026-09-15T16:54:06Z | |
| 2 | + | |
| 3 | +- **Gate**: ✅ **PASS** | |
| 4 | +- **Commit**: `8df88bf` on `feature/acp` | |
| 5 | +- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23) | |
| 6 | +- **Run**: #4 (previous: 2026-09-14T05:30:28Z) | |
| 7 | + | |
| 8 | +## Lint issues (`qlty check`) | |
| 9 | + | |
| 10 | +_none_ | |
| 11 | + | |
| 12 | +### Top rules | |
| 13 | + | |
| 14 | +_none_ | |
| 15 | + | |
| 16 | +### Most affected files | |
| 17 | + | |
| 18 | +_none_ | |
| 19 | + | |
| 20 | +## Code smells (`qlty smells`) | |
| 21 | + | |
| 22 | +Total: **0** (vs previous: ±0) | |
| 23 | + | |
| 24 | +_none_ | |
| 25 | + | |
| 26 | +## Metrics (`qlty metrics`) | |
| 27 | + | |
| 28 | +| metric | total | vs previous | | |
| 29 | +|---|---|---| | |
| 30 | +| funcs | 41 | ±0 | | |
| 31 | +| classes | 2 | ±0 | | |
| 32 | +| fields | 5 | ±0 | | |
| 33 | +| cyclo | 121 | ±0 | | |
| 34 | +| complex | 54 | ±0 | | |
| 35 | +| lcom | 0 | ±0 | | |
| 36 | +| lines | 1012 | +10 | | |
| 37 | +| loc | 493 | +2 | | |
| 38 | + | |
| 39 | +### Most complex files | |
| 40 | + | |
| 41 | +| file | complex | cyclo | loc | | |
| 42 | +|---|---|---|---| | |
| 43 | +| internal/gololang/words.go | 29 | 53 | 193 | | |
| 44 | +| main.go | 16 | 32 | 132 | | |
| 45 | +| internal/gololang/scan.go | 6 | 30 | 81 | | |
| 46 | +| internal/gololang/literals.go | 3 | 4 | 33 | | |
| 47 | +| internal/gololang/gololang.go | 0 | 1 | 48 | | |
| 48 | +| internal/gololang/templates.go | 0 | 1 | 6 | | |
| 49 | + | |
| 50 | +## Trend | |
| 51 | + | |
| 52 | +| run | timestamp | error | warning | smells | complex | gate | | |
| 53 | +|---|---|---|---|---|---|---| | |
| 54 | +| 1 | 2026-09-14T04:44:06Z | 0 | 0 | 2 | 53 | FAIL | | |
| 55 | +| 2 | 2026-09-14T04:44:49Z | 0 | 0 | 0 | 54 | PASS | | |
| 56 | +| 3 | 2026-09-14T05:30:28Z | 0 | 0 | 0 | 54 | PASS | | |
| 57 | +| 4 | 2026-09-15T16:54:06Z | 0 | 0 | 0 | 54 | PASS | | |
| new file mode 100644 | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | +# Quality report — 2026-09-15T16:54:06Z | ||
| 2 | + | ||
| 3 | +- **Gate**: ✅ **PASS** | ||
| 4 | +- **Commit**: `8df88bf` on `feature/acp` | ||
| 5 | +- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23) | ||
| 6 | +- **Run**: #4 (previous: 2026-09-14T05:30:28Z) | ||
| 7 | + | ||
| 8 | +## Lint issues (`qlty check`) | ||
| 9 | + | ||
| 10 | +_none_ | ||
| 11 | + | ||
| 12 | +### Top rules | ||
| 13 | + | ||
| 14 | +_none_ | ||
| 15 | + | ||
| 16 | +### Most affected files | ||
| 17 | + | ||
| 18 | +_none_ | ||
| 19 | + | ||
| 20 | +## Code smells (`qlty smells`) | ||
| 21 | + | ||
| 22 | +Total: **0** (vs previous: ±0) | ||
| 23 | + | ||
| 24 | +_none_ | ||
| 25 | + | ||
| 26 | +## Metrics (`qlty metrics`) | ||
| 27 | + | ||
| 28 | +| metric | total | vs previous | | ||
| 29 | +|---|---|---| | ||
| 30 | +| funcs | 41 | ±0 | | ||
| 31 | +| classes | 2 | ±0 | | ||
| 32 | +| fields | 5 | ±0 | | ||
| 33 | +| cyclo | 121 | ±0 | | ||
| 34 | +| complex | 54 | ±0 | | ||
| 35 | +| lcom | 0 | ±0 | | ||
| 36 | +| lines | 1012 | +10 | | ||
| 37 | +| loc | 493 | +2 | | ||
| 38 | + | ||
| 39 | +### Most complex files | ||
| 40 | + | ||
| 41 | +| file | complex | cyclo | loc | | ||
| 42 | +|---|---|---|---| | ||
| 43 | +| internal/gololang/words.go | 29 | 53 | 193 | | ||
| 44 | +| main.go | 16 | 32 | 132 | | ||
| 45 | +| internal/gololang/scan.go | 6 | 30 | 81 | | ||
| 46 | +| internal/gololang/literals.go | 3 | 4 | 33 | | ||
| 47 | +| internal/gololang/gololang.go | 0 | 1 | 48 | | ||
| 48 | +| internal/gololang/templates.go | 0 | 1 | 6 | | ||
| 49 | + | ||
| 50 | +## Trend | ||
| 51 | + | ||
| 52 | +| run | timestamp | error | warning | smells | complex | gate | | ||
| 53 | +|---|---|---|---|---|---|---| | ||
| 54 | +| 1 | 2026-09-14T04:44:06Z | 0 | 0 | 2 | 53 | FAIL | | ||
| 55 | +| 2 | 2026-09-14T04:44:49Z | 0 | 0 | 0 | 54 | PASS | | ||
| 56 | +| 3 | 2026-09-14T05:30:28Z | 0 | 0 | 0 | 54 | PASS | | ||
| 57 | +| 4 | 2026-09-15T16:54:06Z | 0 | 0 | 0 | 54 | PASS | | ||
added
.vscode/extensions.json +9 -0 | new file mode 100644 | ||
| @@ -0,0 +1,9 @@ | ||
| 1 | +{ | |
| 2 | + "recommendations": [ | |
| 3 | + "ms-azuretools.vscode-docker", | |
| 4 | + "pkief.material-icon-theme", | |
| 5 | + "pkief.material-product-icons", | |
| 6 | + "aaron-bond.better-comments", | |
| 7 | + "bierner.markdown-mermaid", | |
| 8 | + ] | |
| 9 | +} | |
| \ No newline at end of file | ||
| new file mode 100644 | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | +{ | ||
| 2 | + "recommendations": [ | ||
| 3 | + "ms-azuretools.vscode-docker", | ||
| 4 | + "pkief.material-icon-theme", | ||
| 5 | + "pkief.material-product-icons", | ||
| 6 | + "aaron-bond.better-comments", | ||
| 7 | + "bierner.markdown-mermaid", | ||
| 8 | + ] | ||
| 9 | +} | ||
| \ No newline at end of file | \ No newline at end of file | ||
added
.vscode/settings.json +89 -0 | new file mode 100644 | ||
| @@ -0,0 +1,89 @@ | ||
| 1 | +{ | |
| 2 | + "workbench.iconTheme": "material-icon-theme", | |
| 3 | + "workbench.colorTheme": "GitHub Light Colorblind (Beta)", | |
| 4 | + "editor.fontSize": 14, | |
| 5 | + "terminal.integrated.fontSize": 14, | |
| 6 | + "editor.insertSpaces": true, | |
| 7 | + "editor.tabSize": 4, | |
| 8 | + "editor.detectIndentation": true, | |
| 9 | + "files.autoSave": "afterDelay", | |
| 10 | + "files.autoSaveDelay": 1000, | |
| 11 | + // "editor.defaultFormatter": "esbenp.prettier-vscode", | |
| 12 | + "editor.formatOnSave": true, | |
| 13 | + "workbench.tree.indent": 20, | |
| 14 | + //"workbench.activityBar.location": "top", | |
| 15 | + "workbench.editor.showTabs": "multiple", | |
| 16 | + "window.zoomLevel": 0.0, | |
| 17 | + "[markdown]": { | |
| 18 | + "editor.unicodeHighlight.ambiguousCharacters": false, | |
| 19 | + "editor.unicodeHighlight.invisibleCharacters": false, | |
| 20 | + "diffEditor.ignoreTrimWhitespace": false, | |
| 21 | + "editor.fontWeight": "normal", | |
| 22 | + "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace", | |
| 23 | + "editor.fontSize": 14, | |
| 24 | + "editor.wordWrap": "on", | |
| 25 | + "editor.quickSuggestions": { | |
| 26 | + "comments": "off", | |
| 27 | + "strings": "off", | |
| 28 | + "other": "off" | |
| 29 | + } | |
| 30 | + }, | |
| 31 | + "markdown.preview.fontSize": 14, | |
| 32 | + // "workbench.editorAssociations": { | |
| 33 | + // "*.md": "vscode.markdown.preview.editor" | |
| 34 | + // }, | |
| 35 | + "markdown.marp.html": "all", | |
| 36 | + "[dockerfile]": { | |
| 37 | + "editor.fontSize": 14 | |
| 38 | + }, | |
| 39 | + "[dockercompose]": { | |
| 40 | + "editor.fontSize": 14 | |
| 41 | + }, | |
| 42 | + "[json]": { | |
| 43 | + "editor.fontSize": 14 | |
| 44 | + }, | |
| 45 | + "[yaml]": { | |
| 46 | + "editor.fontSize": 14 | |
| 47 | + }, | |
| 48 | + "[go]": { | |
| 49 | + "editor.fontSize": 14, | |
| 50 | + "editor.defaultFormatter": "golang.go", | |
| 51 | + "editor.codeActionsOnSave": { | |
| 52 | + "source.organizeImports": "explicit" | |
| 53 | + } | |
| 54 | + }, | |
| 55 | + "go.lintTool": "golangci-lint", | |
| 56 | + "go.lintOnSave": "package", | |
| 57 | + "go.formatTool": "goimports", | |
| 58 | + "go.useLanguageServer": true, | |
| 59 | + "gopls": { | |
| 60 | + "ui.semanticTokens": true, | |
| 61 | + "ui.completion.usePlaceholders": true | |
| 62 | + }, | |
| 63 | + "workbench.colorCustomizations": { | |
| 64 | + "activityBar.activeBackground": "#ffffff", | |
| 65 | + "activityBar.background": "#ffffff", | |
| 66 | + "activityBar.foreground": "#15202b", | |
| 67 | + "activityBar.inactiveForeground": "#15202b99", | |
| 68 | + "activityBarBadge.background": "#90a5de", | |
| 69 | + "activityBarBadge.foreground": "#15202b", | |
| 70 | + "commandCenter.border": "#15202b99", | |
| 71 | + "sash.hoverBorder": "#ffffff", | |
| 72 | + "statusBar.background": "#b8a8e8", | |
| 73 | + "statusBar.foreground": "#15202b", | |
| 74 | + "statusBarItem.hoverBackground": "#a4f5b0", | |
| 75 | + "statusBarItem.remoteBackground": "#9c8cf2", | |
| 76 | + "statusBarItem.remoteForeground": "#15202b", | |
| 77 | + "titleBar.activeBackground": "#90a5de", | |
| 78 | + "titleBar.activeForeground": "#15202b", | |
| 79 | + "titleBar.inactiveBackground": "#90a5de", | |
| 80 | + "titleBar.inactiveForeground": "#15202b99", | |
| 81 | + "activityBarTop.activeBackground": "#ffffff", | |
| 82 | + "activityBarTop.background": "#ffffff", | |
| 83 | + "activityBarTop.foreground": "#15202b", | |
| 84 | + "activityBarTop.inactiveForeground": "#15202b99", | |
| 85 | + "commandCenter.foreground": "#15202b", | |
| 86 | + "statusBar.debuggingBackground": "#90a5de", | |
| 87 | + "statusBar.debuggingForeground": "#15202b" | |
| 88 | + } | |
| 89 | +} | |
| \ No newline at end of file | ||
| new file mode 100644 | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | +{ | ||
| 2 | + "workbench.iconTheme": "material-icon-theme", | ||
| 3 | + "workbench.colorTheme": "GitHub Light Colorblind (Beta)", | ||
| 4 | + "editor.fontSize": 14, | ||
| 5 | + "terminal.integrated.fontSize": 14, | ||
| 6 | + "editor.insertSpaces": true, | ||
| 7 | + "editor.tabSize": 4, | ||
| 8 | + "editor.detectIndentation": true, | ||
| 9 | + "files.autoSave": "afterDelay", | ||
| 10 | + "files.autoSaveDelay": 1000, | ||
| 11 | + // "editor.defaultFormatter": "esbenp.prettier-vscode", | ||
| 12 | + "editor.formatOnSave": true, | ||
| 13 | + "workbench.tree.indent": 20, | ||
| 14 | + //"workbench.activityBar.location": "top", | ||
| 15 | + "workbench.editor.showTabs": "multiple", | ||
| 16 | + "window.zoomLevel": 0.0, | ||
| 17 | + "[markdown]": { | ||
| 18 | + "editor.unicodeHighlight.ambiguousCharacters": false, | ||
| 19 | + "editor.unicodeHighlight.invisibleCharacters": false, | ||
| 20 | + "diffEditor.ignoreTrimWhitespace": false, | ||
| 21 | + "editor.fontWeight": "normal", | ||
| 22 | + "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace", | ||
| 23 | + "editor.fontSize": 14, | ||
| 24 | + "editor.wordWrap": "on", | ||
| 25 | + "editor.quickSuggestions": { | ||
| 26 | + "comments": "off", | ||
| 27 | + "strings": "off", | ||
| 28 | + "other": "off" | ||
| 29 | + } | ||
| 30 | + }, | ||
| 31 | + "markdown.preview.fontSize": 14, | ||
| 32 | + // "workbench.editorAssociations": { | ||
| 33 | + // "*.md": "vscode.markdown.preview.editor" | ||
| 34 | + // }, | ||
| 35 | + "markdown.marp.html": "all", | ||
| 36 | + "[dockerfile]": { | ||
| 37 | + "editor.fontSize": 14 | ||
| 38 | + }, | ||
| 39 | + "[dockercompose]": { | ||
| 40 | + "editor.fontSize": 14 | ||
| 41 | + }, | ||
| 42 | + "[json]": { | ||
| 43 | + "editor.fontSize": 14 | ||
| 44 | + }, | ||
| 45 | + "[yaml]": { | ||
| 46 | + "editor.fontSize": 14 | ||
| 47 | + }, | ||
| 48 | + "[go]": { | ||
| 49 | + "editor.fontSize": 14, | ||
| 50 | + "editor.defaultFormatter": "golang.go", | ||
| 51 | + "editor.codeActionsOnSave": { | ||
| 52 | + "source.organizeImports": "explicit" | ||
| 53 | + } | ||
| 54 | + }, | ||
| 55 | + "go.lintTool": "golangci-lint", | ||
| 56 | + "go.lintOnSave": "package", | ||
| 57 | + "go.formatTool": "goimports", | ||
| 58 | + "go.useLanguageServer": true, | ||
| 59 | + "gopls": { | ||
| 60 | + "ui.semanticTokens": true, | ||
| 61 | + "ui.completion.usePlaceholders": true | ||
| 62 | + }, | ||
| 63 | + "workbench.colorCustomizations": { | ||
| 64 | + "activityBar.activeBackground": "#ffffff", | ||
| 65 | + "activityBar.background": "#ffffff", | ||
| 66 | + "activityBar.foreground": "#15202b", | ||
| 67 | + "activityBar.inactiveForeground": "#15202b99", | ||
| 68 | + "activityBarBadge.background": "#90a5de", | ||
| 69 | + "activityBarBadge.foreground": "#15202b", | ||
| 70 | + "commandCenter.border": "#15202b99", | ||
| 71 | + "sash.hoverBorder": "#ffffff", | ||
| 72 | + "statusBar.background": "#b8a8e8", | ||
| 73 | + "statusBar.foreground": "#15202b", | ||
| 74 | + "statusBarItem.hoverBackground": "#a4f5b0", | ||
| 75 | + "statusBarItem.remoteBackground": "#9c8cf2", | ||
| 76 | + "statusBarItem.remoteForeground": "#15202b", | ||
| 77 | + "titleBar.activeBackground": "#90a5de", | ||
| 78 | + "titleBar.activeForeground": "#15202b", | ||
| 79 | + "titleBar.inactiveBackground": "#90a5de", | ||
| 80 | + "titleBar.inactiveForeground": "#15202b99", | ||
| 81 | + "activityBarTop.activeBackground": "#ffffff", | ||
| 82 | + "activityBarTop.background": "#ffffff", | ||
| 83 | + "activityBarTop.foreground": "#15202b", | ||
| 84 | + "activityBarTop.inactiveForeground": "#15202b99", | ||
| 85 | + "commandCenter.foreground": "#15202b", | ||
| 86 | + "statusBar.debuggingBackground": "#90a5de", | ||
| 87 | + "statusBar.debuggingForeground": "#15202b" | ||
| 88 | + } | ||
| 89 | +} | ||
| \ No newline at end of file | \ No newline at end of file | ||
added
01-release.tag.sh +115 -0 | new file mode 100755 | ||
| @@ -0,0 +1,115 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +: <<'COMMENT' | |
| 3 | +Releasing turbo-golo is tagging it. Pushing the tag starts the Release workflow, | |
| 4 | +which builds the binaries and publishes the release page with them. | |
| 5 | + | |
| 6 | +1. Set TAG and ABOUT in release.env | |
| 7 | +2. Run this script: ./01-release.tag.sh (make check, commit, push, tag, push the tag) | |
| 8 | +3. Watch the "Release" workflow on Rickub (Actions tab): the tag push starts | |
| 9 | + it; it cross-compiles the binaries with ./02-build-releases.sh and publishes | |
| 10 | + the release page with them, using the job's own token. No personal token is | |
| 11 | + needed, and nothing else has to be run by hand. | |
| 12 | +COMMENT | |
| 13 | + | |
| 14 | +# Without this, a failing step is ignored and the next one runs anyway. That is | |
| 15 | +# not theoretical: `git tag` refusing a tag that already existed was skipped in | |
| 16 | +# silence, and the `git push` below then pushed the OLD tag — so a release was | |
| 17 | +# cut from a commit nobody meant, and 02 was left to notice. | |
| 18 | +set -euo pipefail | |
| 19 | + | |
| 20 | +if [ ! -f release.env ]; then | |
| 21 | + echo "❌ release.env is missing" | |
| 22 | + echo "💡 Create it with the version you are publishing:" | |
| 23 | + echo ' TAG=v1.0.0' | |
| 24 | + echo ' ABOUT="Turbo Golo"' | |
| 25 | + exit 1 | |
| 26 | +fi | |
| 27 | + | |
| 28 | +set -o allexport | |
| 29 | +# shellcheck source=/dev/null | |
| 30 | +source release.env | |
| 31 | +set +o allexport | |
| 32 | + | |
| 33 | +echo "Releasing turbo-golo ${TAG}: ${ABOUT}" | |
| 34 | + | |
| 35 | +# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — and worse than | |
| 36 | +# a typo: the workflow only starts on v*, and the module proxy will not serve | |
| 37 | +# `go install …@TAG` for a tag it cannot read as a version. | |
| 38 | +if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then | |
| 39 | + echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)" | |
| 40 | + exit 1 | |
| 41 | +fi | |
| 42 | + | |
| 43 | +# The whole suite has to pass before a version exists that people will | |
| 44 | +# download. A tag the proxy has cached is the wrong place to find out. | |
| 45 | +# | |
| 46 | +# The marker matters: the suite includes tests that run *this script* against a | |
| 47 | +# throwaway clone, and without it `make check` would run those, which would run | |
| 48 | +# this script, which would run `make check`. They skip themselves when they see | |
| 49 | +# it. Exported, so it reaches the test binary through make and go test. | |
| 50 | +export TURBO_GOLO_RELEASING=1 | |
| 51 | +echo "→ make check" | |
| 52 | +make --no-print-directory check | |
| 53 | + | |
| 54 | +# tagExists reports whether TAG is already taken, here or on the remote. The | |
| 55 | +# remote matters on its own: a tag deleted locally after a failed attempt still | |
| 56 | +# exists there, and pushing a new one at a different commit is rejected. | |
| 57 | +tagExists() { | |
| 58 | + if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then | |
| 59 | + printf 'locally, on %s\n' "$(git rev-parse --short "${TAG}^{commit}")" | |
| 60 | + return 0 | |
| 61 | + fi | |
| 62 | + if ! remote="$(git ls-remote --tags origin "refs/tags/${TAG}" 2>/dev/null)"; then | |
| 63 | + return 1 # the remote is unreachable; the push below will say so | |
| 64 | + fi | |
| 65 | + if [ -n "${remote}" ]; then | |
| 66 | + # No commit is named here on purpose: for an annotated tag ls-remote | |
| 67 | + # gives the tag object, not the commit, and printing that as if it were | |
| 68 | + # the commit sends the reader looking for a SHA they will never find. | |
| 69 | + printf 'on origin\n' | |
| 70 | + return 0 | |
| 71 | + fi | |
| 72 | + return 1 | |
| 73 | +} | |
| 74 | + | |
| 75 | +if where="$(tagExists)"; then | |
| 76 | + echo "❌ ${TAG} already exists ${where}" | |
| 77 | + echo "💡 A published version is not yours to move: the module proxy caches" | |
| 78 | + echo " what it fetched for \`go install\`, and the release page already" | |
| 79 | + echo " carries binaries with that number. Bump TAG in release.env instead." | |
| 80 | + exit 1 | |
| 81 | +fi | |
| 82 | + | |
| 83 | +# The editor must not be published with a replace directive in it: the module | |
| 84 | +# proxy serves the go.mod as written, and `go install …@TAG` would then look | |
| 85 | +# for turbo-core in a directory that does not exist on the installer's machine. | |
| 86 | +if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then | |
| 87 | + echo "❌ go.mod has a replace directive, which a published module must not" | |
| 88 | + grep -nE '^[[:space:]]*replace[[:space:]]' go.mod | |
| 89 | + exit 1 | |
| 90 | +fi | |
| 91 | + | |
| 92 | +find . -name '.DS_Store' -type f -delete | |
| 93 | + | |
| 94 | +git add . | |
| 95 | + | |
| 96 | +# Nothing to commit is not a failure — the work may already be committed — but | |
| 97 | +# under `set -e` a plain `git commit` would stop the release right here. | |
| 98 | +if git diff --cached --quiet; then | |
| 99 | + echo "Nothing to commit; releasing what is already on HEAD" | |
| 100 | +else | |
| 101 | + git commit -m "📦 ${ABOUT}" | |
| 102 | +fi | |
| 103 | + | |
| 104 | +git push origin "$(git rev-parse --abbrev-ref HEAD)" | |
| 105 | + | |
| 106 | +# The tag goes on after the push, so a rejected push never leaves a tag behind | |
| 107 | +# pointing at a commit the remote has never seen. | |
| 108 | +git tag -a "${TAG}" -m "${ABOUT}" | |
| 109 | +git push origin "${TAG}" | |
| 110 | + | |
| 111 | +echo "✅ turbo-golo ${TAG} published" | |
| 112 | +echo "💡 The tag push started the Release workflow; it builds the binaries and" | |
| 113 | +echo " creates the release page. Watch it on the repository's Actions tab." | |
| 114 | +echo "💡 To see what it will build without publishing anything:" | |
| 115 | +echo " ./02-build-releases.sh ${TAG}" | |
| new file mode 100755 | |||
| @@ -0,0 +1,115 @@ | |||
| 1 | +#!/bin/bash | ||
| 2 | +: <<'COMMENT' | ||
| 3 | +Releasing turbo-golo is tagging it. Pushing the tag starts the Release workflow, | ||
| 4 | +which builds the binaries and publishes the release page with them. | ||
| 5 | + | ||
| 6 | +1. Set TAG and ABOUT in release.env | ||
| 7 | +2. Run this script: ./01-release.tag.sh (make check, commit, push, tag, push the tag) | ||
| 8 | +3. Watch the "Release" workflow on Rickub (Actions tab): the tag push starts | ||
| 9 | + it; it cross-compiles the binaries with ./02-build-releases.sh and publishes | ||
| 10 | + the release page with them, using the job's own token. No personal token is | ||
| 11 | + needed, and nothing else has to be run by hand. | ||
| 12 | +COMMENT | ||
| 13 | + | ||
| 14 | +# Without this, a failing step is ignored and the next one runs anyway. That is | ||
| 15 | +# not theoretical: `git tag` refusing a tag that already existed was skipped in | ||
| 16 | +# silence, and the `git push` below then pushed the OLD tag — so a release was | ||
| 17 | +# cut from a commit nobody meant, and 02 was left to notice. | ||
| 18 | +set -euo pipefail | ||
| 19 | + | ||
| 20 | +if [ ! -f release.env ]; then | ||
| 21 | + echo "❌ release.env is missing" | ||
| 22 | + echo "💡 Create it with the version you are publishing:" | ||
| 23 | + echo ' TAG=v1.0.0' | ||
| 24 | + echo ' ABOUT="Turbo Golo"' | ||
| 25 | + exit 1 | ||
| 26 | +fi | ||
| 27 | + | ||
| 28 | +set -o allexport | ||
| 29 | +# shellcheck source=/dev/null | ||
| 30 | +source release.env | ||
| 31 | +set +o allexport | ||
| 32 | + | ||
| 33 | +echo "Releasing turbo-golo ${TAG}: ${ABOUT}" | ||
| 34 | + | ||
| 35 | +# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — and worse than | ||
| 36 | +# a typo: the workflow only starts on v*, and the module proxy will not serve | ||
| 37 | +# `go install …@TAG` for a tag it cannot read as a version. | ||
| 38 | +if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then | ||
| 39 | + echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)" | ||
| 40 | + exit 1 | ||
| 41 | +fi | ||
| 42 | + | ||
| 43 | +# The whole suite has to pass before a version exists that people will | ||
| 44 | +# download. A tag the proxy has cached is the wrong place to find out. | ||
| 45 | +# | ||
| 46 | +# The marker matters: the suite includes tests that run *this script* against a | ||
| 47 | +# throwaway clone, and without it `make check` would run those, which would run | ||
| 48 | +# this script, which would run `make check`. They skip themselves when they see | ||
| 49 | +# it. Exported, so it reaches the test binary through make and go test. | ||
| 50 | +export TURBO_GOLO_RELEASING=1 | ||
| 51 | +echo "→ make check" | ||
| 52 | +make --no-print-directory check | ||
| 53 | + | ||
| 54 | +# tagExists reports whether TAG is already taken, here or on the remote. The | ||
| 55 | +# remote matters on its own: a tag deleted locally after a failed attempt still | ||
| 56 | +# exists there, and pushing a new one at a different commit is rejected. | ||
| 57 | +tagExists() { | ||
| 58 | + if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then | ||
| 59 | + printf 'locally, on %s\n' "$(git rev-parse --short "${TAG}^{commit}")" | ||
| 60 | + return 0 | ||
| 61 | + fi | ||
| 62 | + if ! remote="$(git ls-remote --tags origin "refs/tags/${TAG}" 2>/dev/null)"; then | ||
| 63 | + return 1 # the remote is unreachable; the push below will say so | ||
| 64 | + fi | ||
| 65 | + if [ -n "${remote}" ]; then | ||
| 66 | + # No commit is named here on purpose: for an annotated tag ls-remote | ||
| 67 | + # gives the tag object, not the commit, and printing that as if it were | ||
| 68 | + # the commit sends the reader looking for a SHA they will never find. | ||
| 69 | + printf 'on origin\n' | ||
| 70 | + return 0 | ||
| 71 | + fi | ||
| 72 | + return 1 | ||
| 73 | +} | ||
| 74 | + | ||
| 75 | +if where="$(tagExists)"; then | ||
| 76 | + echo "❌ ${TAG} already exists ${where}" | ||
| 77 | + echo "💡 A published version is not yours to move: the module proxy caches" | ||
| 78 | + echo " what it fetched for \`go install\`, and the release page already" | ||
| 79 | + echo " carries binaries with that number. Bump TAG in release.env instead." | ||
| 80 | + exit 1 | ||
| 81 | +fi | ||
| 82 | + | ||
| 83 | +# The editor must not be published with a replace directive in it: the module | ||
| 84 | +# proxy serves the go.mod as written, and `go install …@TAG` would then look | ||
| 85 | +# for turbo-core in a directory that does not exist on the installer's machine. | ||
| 86 | +if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then | ||
| 87 | + echo "❌ go.mod has a replace directive, which a published module must not" | ||
| 88 | + grep -nE '^[[:space:]]*replace[[:space:]]' go.mod | ||
| 89 | + exit 1 | ||
| 90 | +fi | ||
| 91 | + | ||
| 92 | +find . -name '.DS_Store' -type f -delete | ||
| 93 | + | ||
| 94 | +git add . | ||
| 95 | + | ||
| 96 | +# Nothing to commit is not a failure — the work may already be committed — but | ||
| 97 | +# under `set -e` a plain `git commit` would stop the release right here. | ||
| 98 | +if git diff --cached --quiet; then | ||
| 99 | + echo "Nothing to commit; releasing what is already on HEAD" | ||
| 100 | +else | ||
| 101 | + git commit -m "📦 ${ABOUT}" | ||
| 102 | +fi | ||
| 103 | + | ||
| 104 | +git push origin "$(git rev-parse --abbrev-ref HEAD)" | ||
| 105 | + | ||
| 106 | +# The tag goes on after the push, so a rejected push never leaves a tag behind | ||
| 107 | +# pointing at a commit the remote has never seen. | ||
| 108 | +git tag -a "${TAG}" -m "${ABOUT}" | ||
| 109 | +git push origin "${TAG}" | ||
| 110 | + | ||
| 111 | +echo "✅ turbo-golo ${TAG} published" | ||
| 112 | +echo "💡 The tag push started the Release workflow; it builds the binaries and" | ||
| 113 | +echo " creates the release page. Watch it on the repository's Actions tab." | ||
| 114 | +echo "💡 To see what it will build without publishing anything:" | ||
| 115 | +echo " ./02-build-releases.sh ${TAG}" | ||
added
02-build-releases.sh +209 -0 | new file mode 100755 | ||
| @@ -0,0 +1,209 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +: <<'COMMENT' | |
| 3 | +Build the release binaries and stage them under release/${TAG}/ | |
| 4 | + | |
| 5 | +Usage: | |
| 6 | + ./02-build-releases.sh # TAG and ABOUT come from release.env | |
| 7 | + ./02-build-releases.sh v1.0.0 # override the tag for this run (what CI does) | |
| 8 | + | |
| 9 | +This is the script the Release workflow runs when ./01-release.tag.sh pushes a | |
| 10 | +tag; the workflow then attaches everything staged here to the release page. | |
| 11 | +Nothing here publishes anything, so it is also the way to see what a release | |
| 12 | +will contain before cutting it, or to build the binaries by hand. | |
| 13 | + | |
| 14 | +Only the Go toolchain and git are needed. The same command works on a laptop | |
| 15 | +and in a Rickub CI job. | |
| 16 | + | |
| 17 | +What ends up in release/${TAG}/: | |
| 18 | + turbo-golo-<version>-<os>-<arch>[.exe] one binary per platform in PLATFORMS | |
| 19 | + SHA256SUMS checksums of every binary | |
| 20 | + README.md the downloads, how to run and verify them | |
| 21 | +COMMENT | |
| 22 | + | |
| 23 | +set -euo pipefail | |
| 24 | + | |
| 25 | +# release.env carries TAG ("v1.0.0") and ABOUT (the one-line description). It | |
| 26 | +# is git-ignored (*.env), so a CI job does not have it: there the tag comes | |
| 27 | +# from the command line and ABOUT from the environment, or defaults to the | |
| 28 | +# tag. A tag given on the command line always wins, so a test build never | |
| 29 | +# edits the file. | |
| 30 | +if [ -f release.env ]; then | |
| 31 | + # shellcheck source=/dev/null | |
| 32 | + source release.env | |
| 33 | +fi | |
| 34 | +TAG="${1:-${TAG:-}}" | |
| 35 | +ABOUT="${ABOUT:-Turbo Golo ${TAG}}" | |
| 36 | + | |
| 37 | +# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — and for a Go | |
| 38 | +# module it is worse than a typo: the proxy will not serve a tag it cannot read | |
| 39 | +# as a version, so `go install` would fail on a release that built perfectly. | |
| 40 | +if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then | |
| 41 | + echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)" | |
| 42 | + exit 1 | |
| 43 | +fi | |
| 44 | + | |
| 45 | +# 01 refuses this too, but CI runs *this* script on a tag that is already | |
| 46 | +# pushed, without ever running 01. The proxy serves go.mod as written, so a | |
| 47 | +# published editor carrying a replace tells `go install` to look for turbo-core | |
| 48 | +# in a directory that does not exist on the installer's machine. | |
| 49 | +if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then | |
| 50 | + echo "❌ go.mod has a replace directive, which a published module must not" | |
| 51 | + grep -nE '^[[:space:]]*replace[[:space:]]' go.mod | |
| 52 | + exit 1 | |
| 53 | +fi | |
| 54 | + | |
| 55 | +# The platforms a release is built for. Add or remove a line and everything | |
| 56 | +# below follows: the builds, the checksums and the README. | |
| 57 | +PLATFORMS=( | |
| 58 | + "darwin/arm64" | |
| 59 | + "linux/amd64" | |
| 60 | + "linux/arm64" | |
| 61 | + "windows/amd64" | |
| 62 | + "windows/arm64" | |
| 63 | +) | |
| 64 | + | |
| 65 | +echo "🚀 Building Turbo Golo ${TAG} — ${ABOUT}" | |
| 66 | +echo "🐹 $(go version)" | |
| 67 | + | |
| 68 | +RELEASES_DIR="release/${TAG}" | |
| 69 | + | |
| 70 | +# The tag is "v1.0.0"; the assets carry the bare version, "1.0.0". | |
| 71 | +VERSION="${TAG#v}" | |
| 72 | + | |
| 73 | +# Where the Makefile puts the binary for this machine. | |
| 74 | +BUILT="bin/turbo-golo" | |
| 75 | + | |
| 76 | +# The release is ${TAG}, so ${TAG} is what every binary here reports. The | |
| 77 | +# Makefile's own default comes from `git describe`, which answers a different | |
| 78 | +# question — where HEAD is — and disagrees the moment you commit after tagging. | |
| 79 | +# Overriding VERSION keeps the -X paths defined in one place all the same. | |
| 80 | +LDFLAGS="$(make --no-print-directory ldflags VERSION="${TAG}")" | |
| 81 | + | |
| 82 | +# A fresh directory, so a binary left by an earlier run for a platform since | |
| 83 | +# removed from PLATFORMS cannot end up on the release page. | |
| 84 | +rm -rf "${RELEASES_DIR}" | |
| 85 | +mkdir -p "${RELEASES_DIR}" | |
| 86 | + | |
| 87 | +# The host build comes first: it is the quickest way to find a compile error, | |
| 88 | +# before spending five cross-compiles on it. | |
| 89 | +make build VERSION="${TAG}" | |
| 90 | + | |
| 91 | +if [ ! -f "${BUILT}" ]; then | |
| 92 | + echo "❌ make build produced no ${BUILT}" | |
| 93 | + exit 1 | |
| 94 | +fi | |
| 95 | + | |
| 96 | +# assetName returns what the binary for a platform is called once staged. | |
| 97 | +# Windows executables carry .exe, or Windows will not run them. | |
| 98 | +assetName() { | |
| 99 | + local goos=$1 goarch=$2 | |
| 100 | + local name="turbo-golo-${VERSION}-${goos}-${goarch}" | |
| 101 | + | |
| 102 | + if [ "${goos}" = "windows" ]; then | |
| 103 | + name="${name}.exe" | |
| 104 | + fi | |
| 105 | + printf '%s\n' "${name}" | |
| 106 | +} | |
| 107 | + | |
| 108 | +echo "" | |
| 109 | +echo "🔨 Cross-compiling for ${#PLATFORMS[@]} platforms..." | |
| 110 | + | |
| 111 | +for platform in "${PLATFORMS[@]}"; do | |
| 112 | + goos="${platform%/*}" | |
| 113 | + goarch="${platform#*/}" | |
| 114 | + asset="$(assetName "${goos}" "${goarch}")" | |
| 115 | + | |
| 116 | + # CGO_ENABLED=0 because there is nothing to link against on the other side | |
| 117 | + # of a cross-compile, and this project needs no C at all: tcell and toml | |
| 118 | + # are both pure Go. | |
| 119 | + # | |
| 120 | + # -trimpath keeps the paths of this machine out of a binary that goes to | |
| 121 | + # strangers. | |
| 122 | + # | |
| 123 | + # -ldflags is what makes a downloaded binary agree with the release it came | |
| 124 | + # from. Without it the Go build system names the build itself, and every | |
| 125 | + # asset here would report "devel" while the release page says ${TAG}. | |
| 126 | + if ! CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \ | |
| 127 | + go build -trimpath -ldflags "${LDFLAGS}" -o "${RELEASES_DIR}/${asset}" .; then | |
| 128 | + echo " ❌ ${platform}" | |
| 129 | + exit 1 | |
| 130 | + fi | |
| 131 | + echo " ✅ ${asset}" | |
| 132 | +done | |
| 133 | + | |
| 134 | +# The staged asset for this machine is the only one that can be run here, and | |
| 135 | +# running it is the only proof that what ships carries the version rather than | |
| 136 | +# that the flags looked right. | |
| 137 | +# The number has to *equal* the tag, not merely appear in the output: "0.2.0" | |
| 138 | +# is a substring of "10.2.0" and of a commit hash that happens to contain it, | |
| 139 | +# and a stamp that is nearly right is the failure worth catching. | |
| 140 | +HOST_ASSET="$(assetName "$(go env GOOS)" "$(go env GOARCH)")" | |
| 141 | +if [ -x "${RELEASES_DIR}/${HOST_ASSET}" ]; then | |
| 142 | + if ! reported="$(scripts/check-version.sh "${RELEASES_DIR}/${HOST_ASSET}" "${TAG}")"; then | |
| 143 | + exit 1 | |
| 144 | + fi | |
| 145 | + echo " ✅ ${HOST_ASSET} reports ${reported}" | |
| 146 | +fi | |
| 147 | + | |
| 148 | +# checksum runs whichever of the two tools this machine has: sha256sum on | |
| 149 | +# Linux, shasum on macOS. | |
| 150 | +checksum() { | |
| 151 | + if command -v sha256sum >/dev/null 2>&1; then | |
| 152 | + sha256sum "$@" | |
| 153 | + else | |
| 154 | + shasum -a 256 "$@" | |
| 155 | + fi | |
| 156 | +} | |
| 157 | + | |
| 158 | +# The workflow attaches SHA256SUMS beside the binaries, so it is written here. | |
| 159 | +# One file covers every platform, which is what "sha256sum -c" expects to | |
| 160 | +# read, and the names carry no directory so it works next to the downloads. | |
| 161 | +(cd "${RELEASES_DIR}" && checksum turbo-golo-"${VERSION}"-* >SHA256SUMS) | |
| 162 | +echo " ✅ SHA256SUMS" | |
| 163 | + | |
| 164 | +# downloadTable lists the platforms as a Markdown table, so the README grows | |
| 165 | +# and shrinks with PLATFORMS rather than repeating it by hand. | |
| 166 | +downloadTable() { | |
| 167 | + printf '| Platform | Download |\n|---|---|\n' | |
| 168 | + for platform in "${PLATFORMS[@]}"; do | |
| 169 | + local goos="${platform%/*}" goarch="${platform#*/}" | |
| 170 | + printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")" | |
| 171 | + done | |
| 172 | +} | |
| 173 | + | |
| 174 | +cat >"${RELEASES_DIR}/README.md" <<EOM | |
| 175 | +# Turbo Golo ${TAG} | |
| 176 | + | |
| 177 | +${ABOUT} | |
| 178 | + | |
| 179 | +Built with $(go env GOVERSION). No runtime dependencies; \`golo\` (GoloScript) is optional and | |
| 180 | +only completion and error marks need it — the interpreter is also the language server. | |
| 181 | + | |
| 182 | +$(downloadTable) | |
| 183 | + | |
| 184 | +## Running it | |
| 185 | + | |
| 186 | + chmod +x turbo-golo-${VERSION}-<platform> | |
| 187 | + ./turbo-golo-${VERSION}-<platform> main.golo | |
| 188 | + | |
| 189 | +On macOS, an unsigned download is quarantined until you say otherwise: | |
| 190 | + | |
| 191 | + xattr -d com.apple.quarantine turbo-golo-${VERSION}-darwin-arm64 | |
| 192 | + | |
| 193 | +## Installing from the module proxy instead | |
| 194 | + | |
| 195 | + go install $(go list -m)@${TAG} | |
| 196 | + | |
| 197 | +## Verifying the download | |
| 198 | + | |
| 199 | + sha256sum -c SHA256SUMS --ignore-missing # shasum -a 256 -c on macOS | |
| 200 | + | |
| 201 | +EOM | |
| 202 | +echo " ✅ README.md" | |
| 203 | + | |
| 204 | +echo "" | |
| 205 | +echo "✨ Build complete!" | |
| 206 | +ls -lh "${RELEASES_DIR}" | |
| 207 | +echo "" | |
| 208 | +echo "💡 Nothing was published. The Release workflow runs this same script when" | |
| 209 | +echo " ./01-release.tag.sh pushes ${TAG}, and attaches release/${TAG}/ to the release page." | |
| new file mode 100755 | |||
| @@ -0,0 +1,209 @@ | |||
| 1 | +#!/bin/bash | ||
| 2 | +: <<'COMMENT' | ||
| 3 | +Build the release binaries and stage them under release/${TAG}/ | ||
| 4 | + | ||
| 5 | +Usage: | ||
| 6 | + ./02-build-releases.sh # TAG and ABOUT come from release.env | ||
| 7 | + ./02-build-releases.sh v1.0.0 # override the tag for this run (what CI does) | ||
| 8 | + | ||
| 9 | +This is the script the Release workflow runs when ./01-release.tag.sh pushes a | ||
| 10 | +tag; the workflow then attaches everything staged here to the release page. | ||
| 11 | +Nothing here publishes anything, so it is also the way to see what a release | ||
| 12 | +will contain before cutting it, or to build the binaries by hand. | ||
| 13 | + | ||
| 14 | +Only the Go toolchain and git are needed. The same command works on a laptop | ||
| 15 | +and in a Rickub CI job. | ||
| 16 | + | ||
| 17 | +What ends up in release/${TAG}/: | ||
| 18 | + turbo-golo-<version>-<os>-<arch>[.exe] one binary per platform in PLATFORMS | ||
| 19 | + SHA256SUMS checksums of every binary | ||
| 20 | + README.md the downloads, how to run and verify them | ||
| 21 | +COMMENT | ||
| 22 | + | ||
| 23 | +set -euo pipefail | ||
| 24 | + | ||
| 25 | +# release.env carries TAG ("v1.0.0") and ABOUT (the one-line description). It | ||
| 26 | +# is git-ignored (*.env), so a CI job does not have it: there the tag comes | ||
| 27 | +# from the command line and ABOUT from the environment, or defaults to the | ||
| 28 | +# tag. A tag given on the command line always wins, so a test build never | ||
| 29 | +# edits the file. | ||
| 30 | +if [ -f release.env ]; then | ||
| 31 | + # shellcheck source=/dev/null | ||
| 32 | + source release.env | ||
| 33 | +fi | ||
| 34 | +TAG="${1:-${TAG:-}}" | ||
| 35 | +ABOUT="${ABOUT:-Turbo Golo ${TAG}}" | ||
| 36 | + | ||
| 37 | +# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — and for a Go | ||
| 38 | +# module it is worse than a typo: the proxy will not serve a tag it cannot read | ||
| 39 | +# as a version, so `go install` would fail on a release that built perfectly. | ||
| 40 | +if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then | ||
| 41 | + echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)" | ||
| 42 | + exit 1 | ||
| 43 | +fi | ||
| 44 | + | ||
| 45 | +# 01 refuses this too, but CI runs *this* script on a tag that is already | ||
| 46 | +# pushed, without ever running 01. The proxy serves go.mod as written, so a | ||
| 47 | +# published editor carrying a replace tells `go install` to look for turbo-core | ||
| 48 | +# in a directory that does not exist on the installer's machine. | ||
| 49 | +if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then | ||
| 50 | + echo "❌ go.mod has a replace directive, which a published module must not" | ||
| 51 | + grep -nE '^[[:space:]]*replace[[:space:]]' go.mod | ||
| 52 | + exit 1 | ||
| 53 | +fi | ||
| 54 | + | ||
| 55 | +# The platforms a release is built for. Add or remove a line and everything | ||
| 56 | +# below follows: the builds, the checksums and the README. | ||
| 57 | +PLATFORMS=( | ||
| 58 | + "darwin/arm64" | ||
| 59 | + "linux/amd64" | ||
| 60 | + "linux/arm64" | ||
| 61 | + "windows/amd64" | ||
| 62 | + "windows/arm64" | ||
| 63 | +) | ||
| 64 | + | ||
| 65 | +echo "🚀 Building Turbo Golo ${TAG} — ${ABOUT}" | ||
| 66 | +echo "🐹 $(go version)" | ||
| 67 | + | ||
| 68 | +RELEASES_DIR="release/${TAG}" | ||
| 69 | + | ||
| 70 | +# The tag is "v1.0.0"; the assets carry the bare version, "1.0.0". | ||
| 71 | +VERSION="${TAG#v}" | ||
| 72 | + | ||
| 73 | +# Where the Makefile puts the binary for this machine. | ||
| 74 | +BUILT="bin/turbo-golo" | ||
| 75 | + | ||
| 76 | +# The release is ${TAG}, so ${TAG} is what every binary here reports. The | ||
| 77 | +# Makefile's own default comes from `git describe`, which answers a different | ||
| 78 | +# question — where HEAD is — and disagrees the moment you commit after tagging. | ||
| 79 | +# Overriding VERSION keeps the -X paths defined in one place all the same. | ||
| 80 | +LDFLAGS="$(make --no-print-directory ldflags VERSION="${TAG}")" | ||
| 81 | + | ||
| 82 | +# A fresh directory, so a binary left by an earlier run for a platform since | ||
| 83 | +# removed from PLATFORMS cannot end up on the release page. | ||
| 84 | +rm -rf "${RELEASES_DIR}" | ||
| 85 | +mkdir -p "${RELEASES_DIR}" | ||
| 86 | + | ||
| 87 | +# The host build comes first: it is the quickest way to find a compile error, | ||
| 88 | +# before spending five cross-compiles on it. | ||
| 89 | +make build VERSION="${TAG}" | ||
| 90 | + | ||
| 91 | +if [ ! -f "${BUILT}" ]; then | ||
| 92 | + echo "❌ make build produced no ${BUILT}" | ||
| 93 | + exit 1 | ||
| 94 | +fi | ||
| 95 | + | ||
| 96 | +# assetName returns what the binary for a platform is called once staged. | ||
| 97 | +# Windows executables carry .exe, or Windows will not run them. | ||
| 98 | +assetName() { | ||
| 99 | + local goos=$1 goarch=$2 | ||
| 100 | + local name="turbo-golo-${VERSION}-${goos}-${goarch}" | ||
| 101 | + | ||
| 102 | + if [ "${goos}" = "windows" ]; then | ||
| 103 | + name="${name}.exe" | ||
| 104 | + fi | ||
| 105 | + printf '%s\n' "${name}" | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +echo "" | ||
| 109 | +echo "🔨 Cross-compiling for ${#PLATFORMS[@]} platforms..." | ||
| 110 | + | ||
| 111 | +for platform in "${PLATFORMS[@]}"; do | ||
| 112 | + goos="${platform%/*}" | ||
| 113 | + goarch="${platform#*/}" | ||
| 114 | + asset="$(assetName "${goos}" "${goarch}")" | ||
| 115 | + | ||
| 116 | + # CGO_ENABLED=0 because there is nothing to link against on the other side | ||
| 117 | + # of a cross-compile, and this project needs no C at all: tcell and toml | ||
| 118 | + # are both pure Go. | ||
| 119 | + # | ||
| 120 | + # -trimpath keeps the paths of this machine out of a binary that goes to | ||
| 121 | + # strangers. | ||
| 122 | + # | ||
| 123 | + # -ldflags is what makes a downloaded binary agree with the release it came | ||
| 124 | + # from. Without it the Go build system names the build itself, and every | ||
| 125 | + # asset here would report "devel" while the release page says ${TAG}. | ||
| 126 | + if ! CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \ | ||
| 127 | + go build -trimpath -ldflags "${LDFLAGS}" -o "${RELEASES_DIR}/${asset}" .; then | ||
| 128 | + echo " ❌ ${platform}" | ||
| 129 | + exit 1 | ||
| 130 | + fi | ||
| 131 | + echo " ✅ ${asset}" | ||
| 132 | +done | ||
| 133 | + | ||
| 134 | +# The staged asset for this machine is the only one that can be run here, and | ||
| 135 | +# running it is the only proof that what ships carries the version rather than | ||
| 136 | +# that the flags looked right. | ||
| 137 | +# The number has to *equal* the tag, not merely appear in the output: "0.2.0" | ||
| 138 | +# is a substring of "10.2.0" and of a commit hash that happens to contain it, | ||
| 139 | +# and a stamp that is nearly right is the failure worth catching. | ||
| 140 | +HOST_ASSET="$(assetName "$(go env GOOS)" "$(go env GOARCH)")" | ||
| 141 | +if [ -x "${RELEASES_DIR}/${HOST_ASSET}" ]; then | ||
| 142 | + if ! reported="$(scripts/check-version.sh "${RELEASES_DIR}/${HOST_ASSET}" "${TAG}")"; then | ||
| 143 | + exit 1 | ||
| 144 | + fi | ||
| 145 | + echo " ✅ ${HOST_ASSET} reports ${reported}" | ||
| 146 | +fi | ||
| 147 | + | ||
| 148 | +# checksum runs whichever of the two tools this machine has: sha256sum on | ||
| 149 | +# Linux, shasum on macOS. | ||
| 150 | +checksum() { | ||
| 151 | + if command -v sha256sum >/dev/null 2>&1; then | ||
| 152 | + sha256sum "$@" | ||
| 153 | + else | ||
| 154 | + shasum -a 256 "$@" | ||
| 155 | + fi | ||
| 156 | +} | ||
| 157 | + | ||
| 158 | +# The workflow attaches SHA256SUMS beside the binaries, so it is written here. | ||
| 159 | +# One file covers every platform, which is what "sha256sum -c" expects to | ||
| 160 | +# read, and the names carry no directory so it works next to the downloads. | ||
| 161 | +(cd "${RELEASES_DIR}" && checksum turbo-golo-"${VERSION}"-* >SHA256SUMS) | ||
| 162 | +echo " ✅ SHA256SUMS" | ||
| 163 | + | ||
| 164 | +# downloadTable lists the platforms as a Markdown table, so the README grows | ||
| 165 | +# and shrinks with PLATFORMS rather than repeating it by hand. | ||
| 166 | +downloadTable() { | ||
| 167 | + printf '| Platform | Download |\n|---|---|\n' | ||
| 168 | + for platform in "${PLATFORMS[@]}"; do | ||
| 169 | + local goos="${platform%/*}" goarch="${platform#*/}" | ||
| 170 | + printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")" | ||
| 171 | + done | ||
| 172 | +} | ||
| 173 | + | ||
| 174 | +cat >"${RELEASES_DIR}/README.md" <<EOM | ||
| 175 | +# Turbo Golo ${TAG} | ||
| 176 | + | ||
| 177 | +${ABOUT} | ||
| 178 | + | ||
| 179 | +Built with $(go env GOVERSION). No runtime dependencies; \`golo\` (GoloScript) is optional and | ||
| 180 | +only completion and error marks need it — the interpreter is also the language server. | ||
| 181 | + | ||
| 182 | +$(downloadTable) | ||
| 183 | + | ||
| 184 | +## Running it | ||
| 185 | + | ||
| 186 | + chmod +x turbo-golo-${VERSION}-<platform> | ||
| 187 | + ./turbo-golo-${VERSION}-<platform> main.golo | ||
| 188 | + | ||
| 189 | +On macOS, an unsigned download is quarantined until you say otherwise: | ||
| 190 | + | ||
| 191 | + xattr -d com.apple.quarantine turbo-golo-${VERSION}-darwin-arm64 | ||
| 192 | + | ||
| 193 | +## Installing from the module proxy instead | ||
| 194 | + | ||
| 195 | + go install $(go list -m)@${TAG} | ||
| 196 | + | ||
| 197 | +## Verifying the download | ||
| 198 | + | ||
| 199 | + sha256sum -c SHA256SUMS --ignore-missing # shasum -a 256 -c on macOS | ||
| 200 | + | ||
| 201 | +EOM | ||
| 202 | +echo " ✅ README.md" | ||
| 203 | + | ||
| 204 | +echo "" | ||
| 205 | +echo "✨ Build complete!" | ||
| 206 | +ls -lh "${RELEASES_DIR}" | ||
| 207 | +echo "" | ||
| 208 | +echo "💡 Nothing was published. The Release workflow runs this same script when" | ||
| 209 | +echo " ./01-release.tag.sh pushes ${TAG}, and attaches release/${TAG}/ to the release page." | ||
added
LICENSE +18 -0 | new file mode 100644 | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) 2026 turbo-editors | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and | |
| 6 | +associated documentation files (the "Software"), to deal in the Software without restriction, including | |
| 7 | +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 8 | +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the | |
| 9 | +following conditions: | |
| 10 | + | |
| 11 | +The above copyright notice and this permission notice shall be included in all copies or substantial | |
| 12 | +portions of the Software. | |
| 13 | + | |
| 14 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT | |
| 15 | +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO | |
| 16 | +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | |
| 17 | +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE | |
| 18 | +USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| new file mode 100644 | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | +MIT License | ||
| 2 | + | ||
| 3 | +Copyright (c) 2026 turbo-editors | ||
| 4 | + | ||
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and | ||
| 6 | +associated documentation files (the "Software"), to deal in the Software without restriction, including | ||
| 7 | +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 8 | +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the | ||
| 9 | +following conditions: | ||
| 10 | + | ||
| 11 | +The above copyright notice and this permission notice shall be included in all copies or substantial | ||
| 12 | +portions of the Software. | ||
| 13 | + | ||
| 14 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT | ||
| 15 | +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO | ||
| 16 | +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | ||
| 17 | +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE | ||
| 18 | +USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
added
Makefile +75 -0 | new file mode 100644 | ||
| @@ -0,0 +1,75 @@ | ||
| 1 | +BINARY := turbo-golo | |
| 2 | +BUILD_DIR := bin | |
| 3 | + | |
| 4 | +# The version the binary reports is stamped in by the linker, so that a release | |
| 5 | +# cannot ship an About box still naming the previous one. git describe gives | |
| 6 | +# the last tag, how far past it this is, and the commit; a checkout with no | |
| 7 | +# tags, or no git at all, falls back to "devel". | |
| 8 | +VERSION_PKG := rickub.com/turbo-editors/turbo-core/version | |
| 9 | +VERSION := $(shell git describe --tags --dirty 2>/dev/null || echo devel) | |
| 10 | +COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null) | |
| 11 | +BUILT := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) | |
| 12 | +LDFLAGS := -X '$(VERSION_PKG).stamp=$(VERSION)' \ | |
| 13 | + -X '$(VERSION_PKG).commit=$(COMMIT)' \ | |
| 14 | + -X '$(VERSION_PKG).built=$(BUILT)' | |
| 15 | + | |
| 16 | +.DEFAULT_GOAL := help | |
| 17 | + | |
| 18 | +## help: list the available targets | |
| 19 | +help: | |
| 20 | + @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## //' | |
| 21 | + | |
| 22 | +## test: run the whole test suite | |
| 23 | +test: | |
| 24 | + go test ./... | |
| 25 | + | |
| 26 | +## test-verbose: run the whole test suite, naming every test | |
| 27 | +test-verbose: | |
| 28 | + go test -v ./... | |
| 29 | + | |
| 30 | +## cover: run the tests and report statement coverage per package | |
| 31 | +cover: | |
| 32 | + go test -cover ./... | |
| 33 | + | |
| 34 | +## build: compile the editor into bin/turbo-golo, then check it reports its version | |
| 35 | +build: | |
| 36 | + go build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY) . | |
| 37 | + @scripts/check-version.sh $(BUILD_DIR)/$(BINARY) "$(VERSION)" "$(COMMIT)" | |
| 38 | + | |
| 39 | +## version: print the version this checkout would build | |
| 40 | +version: | |
| 41 | + @echo "$(VERSION) ($(COMMIT))" | |
| 42 | + | |
| 43 | +## ldflags: print the linker flags a stamped build uses | |
| 44 | +## (03-build-releases.sh reads this, so the stamp is defined once) | |
| 45 | +ldflags: | |
| 46 | + @printf '%s\n' "$(LDFLAGS)" | |
| 47 | + | |
| 48 | +## install: build and install turbo-golo where your shell can find it | |
| 49 | +install: | |
| 50 | + @scripts/install.sh | |
| 51 | + | |
| 52 | +## uninstall: remove an installed turbo-golo | |
| 53 | +uninstall: | |
| 54 | + @scripts/install.sh --uninstall | |
| 55 | + | |
| 56 | +## run: build and start the editor (make run FILE=main.golo) | |
| 57 | +run: build | |
| 58 | + ./$(BUILD_DIR)/$(BINARY) $(FILE) | |
| 59 | + | |
| 60 | +## fmt: format every Go file in place | |
| 61 | +fmt: | |
| 62 | + go fmt ./... | |
| 63 | + | |
| 64 | +## vet: run the standard Go static checks | |
| 65 | +vet: | |
| 66 | + go vet ./... | |
| 67 | + | |
| 68 | +## check: format, vet and test — what to run before committing | |
| 69 | +check: fmt vet test | |
| 70 | + | |
| 71 | +## clean: remove build artefacts | |
| 72 | +clean: | |
| 73 | + rm -rf $(BUILD_DIR) | |
| 74 | + | |
| 75 | +.PHONY: help test test-verbose cover build version ldflags install uninstall run fmt vet check clean | |
| new file mode 100644 | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | +BINARY := turbo-golo | ||
| 2 | +BUILD_DIR := bin | ||
| 3 | + | ||
| 4 | +# The version the binary reports is stamped in by the linker, so that a release | ||
| 5 | +# cannot ship an About box still naming the previous one. git describe gives | ||
| 6 | +# the last tag, how far past it this is, and the commit; a checkout with no | ||
| 7 | +# tags, or no git at all, falls back to "devel". | ||
| 8 | +VERSION_PKG := rickub.com/turbo-editors/turbo-core/version | ||
| 9 | +VERSION := $(shell git describe --tags --dirty 2>/dev/null || echo devel) | ||
| 10 | +COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null) | ||
| 11 | +BUILT := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) | ||
| 12 | +LDFLAGS := -X '$(VERSION_PKG).stamp=$(VERSION)' \ | ||
| 13 | + -X '$(VERSION_PKG).commit=$(COMMIT)' \ | ||
| 14 | + -X '$(VERSION_PKG).built=$(BUILT)' | ||
| 15 | + | ||
| 16 | +.DEFAULT_GOAL := help | ||
| 17 | + | ||
| 18 | +## help: list the available targets | ||
| 19 | +help: | ||
| 20 | + @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## //' | ||
| 21 | + | ||
| 22 | +## test: run the whole test suite | ||
| 23 | +test: | ||
| 24 | + go test ./... | ||
| 25 | + | ||
| 26 | +## test-verbose: run the whole test suite, naming every test | ||
| 27 | +test-verbose: | ||
| 28 | + go test -v ./... | ||
| 29 | + | ||
| 30 | +## cover: run the tests and report statement coverage per package | ||
| 31 | +cover: | ||
| 32 | + go test -cover ./... | ||
| 33 | + | ||
| 34 | +## build: compile the editor into bin/turbo-golo, then check it reports its version | ||
| 35 | +build: | ||
| 36 | + go build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY) . | ||
| 37 | + @scripts/check-version.sh $(BUILD_DIR)/$(BINARY) "$(VERSION)" "$(COMMIT)" | ||
| 38 | + | ||
| 39 | +## version: print the version this checkout would build | ||
| 40 | +version: | ||
| 41 | + @echo "$(VERSION) ($(COMMIT))" | ||
| 42 | + | ||
| 43 | +## ldflags: print the linker flags a stamped build uses | ||
| 44 | +## (03-build-releases.sh reads this, so the stamp is defined once) | ||
| 45 | +ldflags: | ||
| 46 | + @printf '%s\n' "$(LDFLAGS)" | ||
| 47 | + | ||
| 48 | +## install: build and install turbo-golo where your shell can find it | ||
| 49 | +install: | ||
| 50 | + @scripts/install.sh | ||
| 51 | + | ||
| 52 | +## uninstall: remove an installed turbo-golo | ||
| 53 | +uninstall: | ||
| 54 | + @scripts/install.sh --uninstall | ||
| 55 | + | ||
| 56 | +## run: build and start the editor (make run FILE=main.golo) | ||
| 57 | +run: build | ||
| 58 | + ./$(BUILD_DIR)/$(BINARY) $(FILE) | ||
| 59 | + | ||
| 60 | +## fmt: format every Go file in place | ||
| 61 | +fmt: | ||
| 62 | + go fmt ./... | ||
| 63 | + | ||
| 64 | +## vet: run the standard Go static checks | ||
| 65 | +vet: | ||
| 66 | + go vet ./... | ||
| 67 | + | ||
| 68 | +## check: format, vet and test — what to run before committing | ||
| 69 | +check: fmt vet test | ||
| 70 | + | ||
| 71 | +## clean: remove build artefacts | ||
| 72 | +clean: | ||
| 73 | + rm -rf $(BUILD_DIR) | ||
| 74 | + | ||
| 75 | +.PHONY: help test test-verbose cover build version ldflags install uninstall run fmt vet check clean | ||
added
README.md +111 -0 | new file mode 100644 | ||
| @@ -0,0 +1,111 @@ | ||
| 1 | +# turbo-golo | |
| 2 | + | |
| 3 | +A Turbo C-style editor for Golo, written in Go. | |
| 4 | + | |
| 5 | +Built on **[turbo-core](https://rickub.com/turbo-editors/turbo-core)**, the library every Turbo editor shares. What is in this repository is the command, the profile that says this editor is for Golo, and the Golo scanner — about a thousand lines, comments and all. Everything else lives in the library. | |
| 6 | + | |
| 7 | +A full-screen terminal IDE with the Borland furniture — a menu bar with hot keys, movable windows that cast shadows, modal dialogs, a clickable status bar — and the things a Golo editor needs: syntax colouring written against GoloScript's own lexer, loadable colour themes, completion and diagnostics from `golo lsp`, shell windows, per-project settings, a project tree, snippets, and the GoloScript toolchain a menu away. | |
| 8 | + | |
| 9 | +``` | |
| 10 | + File Edit Search Run Code Options Window Snippets Golo Help | |
| 11 | +╔═[x]═════════════════════════════ hello.golo ══════════════════════════════1═[■]╗ | |
| 12 | +║ 1 module hello.World ▲║ | |
| 13 | +║ 2 ▓║ | |
| 14 | +║ 3 # Greet someone several times ░║ | |
| 15 | +║ 4 struct Greeting = { name, times } ░║ | |
| 16 | +║ 5 ░║ | |
| 17 | +║ 6 function greet = |g| { ░║ | |
| 18 | +║ 7 foreach i in range(1, g: times() + 1) { ░║ | |
| 19 | +║ 8 println("Hello, " + g: name() + "! (" + i + ")") ░║ | |
| 20 | +║ 9 } ▼║ | |
| 21 | +║◄▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░►║ | |
| 22 | +╚════════════════════════════════════════════════════════════════════════════════╝ | |
| 23 | + F1 Describe F2 Save F3 Open F6 Window F7 Next F10 Menu 1:1 LSP: ready | |
| 24 | +``` | |
| 25 | + | |
| 26 | +## Getting started | |
| 27 | + | |
| 28 | +```bash | |
| 29 | +make install | |
| 30 | +``` | |
| 31 | + | |
| 32 | +That builds the editor, puts it where your shell looks for commands, and reports what it found — the Go version it built with, where the binary went, whether that directory is on your `PATH`, and whether `golo` is installed. Then, from any directory of Golo scripts: | |
| 33 | + | |
| 34 | +```bash | |
| 35 | +turbo-golo main.golo | |
| 36 | +``` | |
| 37 | + | |
| 38 | +To build without installing, `make build` leaves the binary in `bin/turbo-golo`. From the module proxy instead of a checkout: `go install rickub.com/turbo-editors/turbo-golo@latest`. | |
| 39 | + | |
| 40 | +For completion and diagnostics, install GoloScript as well — the editor works without it, and says so on the status bar. The language server is the interpreter itself, in `golo lsp` mode, so there is nothing separate to install: a machine that can run Golo can complete Golo. Precompiled binaries are on [the release page](https://codeberg.org/TypeUnsafe/golo-script/releases); [the install guide](docs/en/how-to/install-goloscript.md) covers them, building from source, and how to check the editor really found it. | |
| 41 | + | |
| 42 | +The [tutorial](docs/en/tutorials/getting-started.md) walks through a first session in about ten minutes, and [`demos/`](demos/) holds three Golo programs to open in it — a small one, one with tests, and a tour of every construct the scanner colours. | |
| 43 | + | |
| 44 | +## Features | |
| 45 | + | |
| 46 | +- **Every build knows what it is** — `turbo-golo -version` and **Help ▸ About** name the version, the commit and the build date, stamped in by the linker from `git describe` rather than read from a constant somebody forgot to bump | |
| 47 | +- **Turbo Vision interface** — menu bar with `Alt`-letter hot keys, overlapping movable and resizable windows, modal dialogs, mouse support throughout | |
| 48 | +- **Syntax colouring for nine languages** — Golo by a hand-written scanner that follows GoloScript's lexer: `#` and `----` comments, strings carried to their closing quote wherever it is, `"""` multi-line strings, character literals, the `L` and `F` suffixes, the rule that keeps `1..3` an integer and a range, and names made of any Unicode letter or emoji. Plus TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell scripts from turbo-core | |
| 49 | +- **Types by convention** — a capitalised name is a struct, a union or a variant, because that is how Golo writes them; `Some`, `None`, `Ok` and `Err` are types here and not constants, because in Golo they are variants of ordinary unions from `gololang.Errors` | |
| 50 | +- **Themes** in TOML, eleven embedded — Borland navy, dark grey, paper white, espresso, Catppuccin Frappé and Latte, cobalt, Darcula and IntelliJ Light, and a hueless monochrome in both polarities — and any number of your own, with inheritance between files and between style keys. Every shipped theme is held to its contrast by tests | |
| 51 | +- **Per-project settings** in `.turbo-golo/settings.toml` — pin a theme, turn on automatic saving — created from a menu item, never by itself, and re-read on every save so a change takes effect without a restart | |
| 52 | +- **Completion, hover, go-to-definition, references, symbols and diagnostics** from `golo lsp`, entirely optional. The server is the interpreter, looked for on `PATH` and in `/usr/local/bin`, where GoloScript's installer puts it. It does not answer type definitions, and the editor says so rather than pretending | |
| 53 | +- **Terminal windows** — `F8` opens a real shell in a window, with its own VT/ANSI emulator, scrollback and job control (Linux, macOS and Windows) | |
| 54 | +- **Project tree** — `F9` shows the project's files in a window; walk it with the arrows and press Enter to open one | |
| 55 | +- **Snippets** — a `Snippets` menu built from `.turbo-golo/snippets.toml`, grouped into submenus and filtered by the file you are in; the chosen text is inserted at the cursor and re-indented to match | |
| 56 | +- **The GoloScript toolchain a menu away** — `Alt-G` runs the script, the tests, the debugger, the REPL, `golo new`, `gogolo build` and `wagolo build` from `.turbo-golo/tools.toml`, each showing its output where the tool asked: a popup that fills in as it goes, a terminal window, or an editing window to search. Golo has no manifest, so six of the eight commands ask which file they are for before they run — and a tool naming a `menu` of its own gets that menu on the bar | |
| 57 | +- **Editing** with word movement, block indent, a shared clipboard, and undo that merges a run of typing into one step | |
| 58 | +- **Faithful files** — line endings and the trailing newline are preserved, and saving is atomic | |
| 59 | +- **Automatic saving**, off by default, writing a short while after you stop typing | |
| 60 | + | |
| 61 | +## Commands | |
| 62 | + | |
| 63 | +| Command | What it does | | |
| 64 | +| --- | --- | | |
| 65 | +| `make install` | Build and install onto your `PATH` | | |
| 66 | +| `make build` | Compile into `bin/turbo-golo` | | |
| 67 | +| `make test` | Run the whole test suite | | |
| 68 | +| `make check` | `fmt`, `vet`, then the tests — what a commit should pass | | |
| 69 | +| `make run FILE=x.golo` | Build and start the editor on a file | | |
| 70 | +| `make help` | List every target | | |
| 71 | + | |
| 72 | +```bash | |
| 73 | +turbo-golo [-theme name] [-no-lsp] [file...] | |
| 74 | +turbo-golo -list-themes | |
| 75 | +``` | |
| 76 | + | |
| 77 | +## Documentation | |
| 78 | + | |
| 79 | +Full documentation in **[English](docs/en/)** and **[French](docs/fr/)**, organised by the [Diátaxis](https://diataxis.fr) method: | |
| 80 | + | |
| 81 | +| | | | |
| 82 | +| --- | --- | | |
| 83 | +| **Tutorial** | [Your first Golo program in Turbo Golo](docs/en/tutorials/getting-started.md) | | |
| 84 | +| **How-to** | [install the editor](docs/en/how-to/install.md) · [install GoloScript](docs/en/how-to/install-goloscript.md) · [run the tests](docs/en/how-to/run-the-tests.md) · [enable completion](docs/en/how-to/enable-completion.md) · [write a theme](docs/en/how-to/write-a-theme.md) · [move around a file](docs/en/how-to/navigate-code.md) · [ask about code](docs/en/how-to/ask-about-code.md) · [use a terminal](docs/en/how-to/use-a-terminal.md) · [configure a project](docs/en/how-to/configure-a-project.md) · [browse a project](docs/en/how-to/browse-a-project.md) · [use snippets](docs/en/how-to/use-snippets.md) · [run Golo commands](docs/en/how-to/run-golo-commands.md) · [make a release](docs/en/how-to/make-a-release.md) | | |
| 85 | +| **Reference** | [command line](docs/en/reference/cli.md) · [keyboard](docs/en/reference/keyboard.md) · [menus](docs/en/reference/menus.md) · [theme format](docs/en/reference/themes.md) · [terminal windows](docs/en/reference/terminal.md) · [project settings](docs/en/reference/project-settings.md) · [project tree](docs/en/reference/project-tree.md) · [languages](docs/en/reference/languages.md) · [snippets](docs/en/reference/snippets.md) · [Golo tools](docs/en/reference/golo-tools.md) · [the version number](docs/en/reference/versioning.md) | | |
| 86 | +| **Explanation** | [architecture](docs/en/explanation/architecture.md) · [design decisions](docs/en/explanation/design-decisions.md) · [colouring and completion](docs/en/explanation/colouring-and-completion.md) · [terminal windows](docs/en/explanation/terminal-windows.md) · [project settings](docs/en/explanation/project-settings.md) · [project tree](docs/en/explanation/project-tree.md) · [snippets](docs/en/explanation/snippets.md) · [Golo tools](docs/en/explanation/golo-tools.md) | | |
| 87 | + | |
| 88 | +The library's packages each carry their own `README.md` beside the code, in [turbo-core](https://rickub.com/turbo-editors/turbo-core). | |
| 89 | + | |
| 90 | +## Where the code is | |
| 91 | + | |
| 92 | +| | | | |
| 93 | +| --- | --- | | |
| 94 | +| `main.go` | flags, the terminal, the wiring | | |
| 95 | +| `internal/gololang` | the profile, the Golo scanner, the three starter files | | |
| 96 | +| `demos/` | three Golo programs to open in the editor; each runs under `golo` | | |
| 97 | +| everything else | [turbo-core](https://rickub.com/turbo-editors/turbo-core) | | |
| 98 | + | |
| 99 | +The dependency graph is drawn in [`docs/diagrams/packages.drawio`](docs/diagrams/packages.drawio), checked against `go list` by `diagram_test.go`. | |
| 100 | + | |
| 101 | +## Design in one line | |
| 102 | + | |
| 103 | +Two dependencies — `tcell/v2` and `BurntSushi/toml` — and everything else from the standard library, including the scanner toolkit and the Language Server Protocol client. Both come through turbo-core; this repository adds none of its own. The [design decisions](docs/en/explanation/design-decisions.md) page explains why. | |
| 104 | + | |
| 105 | +## Requirements | |
| 106 | + | |
| 107 | +Go 1.26 or later to build it — the editor is written in Go even though it is an editor for Golo. A terminal with mouse reporting, which is all of them. GoloScript is optional, and is what completion, the error marks and the Golo menu's commands need. | |
| 108 | + | |
| 109 | +## Licence | |
| 110 | + | |
| 111 | +See [LICENSE](LICENSE). | |
| new file mode 100644 | |||
| @@ -0,0 +1,111 @@ | |||
| 1 | +# turbo-golo | ||
| 2 | + | ||
| 3 | +A Turbo C-style editor for Golo, written in Go. | ||
| 4 | + | ||
| 5 | +Built on **[turbo-core](https://rickub.com/turbo-editors/turbo-core)**, the library every Turbo editor shares. What is in this repository is the command, the profile that says this editor is for Golo, and the Golo scanner — about a thousand lines, comments and all. Everything else lives in the library. | ||
| 6 | + | ||
| 7 | +A full-screen terminal IDE with the Borland furniture — a menu bar with hot keys, movable windows that cast shadows, modal dialogs, a clickable status bar — and the things a Golo editor needs: syntax colouring written against GoloScript's own lexer, loadable colour themes, completion and diagnostics from `golo lsp`, shell windows, per-project settings, a project tree, snippets, and the GoloScript toolchain a menu away. | ||
| 8 | + | ||
| 9 | +``` | ||
| 10 | + File Edit Search Run Code Options Window Snippets Golo Help | ||
| 11 | +╔═[x]═════════════════════════════ hello.golo ══════════════════════════════1═[■]╗ | ||
| 12 | +║ 1 module hello.World ▲║ | ||
| 13 | +║ 2 ▓║ | ||
| 14 | +║ 3 # Greet someone several times ░║ | ||
| 15 | +║ 4 struct Greeting = { name, times } ░║ | ||
| 16 | +║ 5 ░║ | ||
| 17 | +║ 6 function greet = |g| { ░║ | ||
| 18 | +║ 7 foreach i in range(1, g: times() + 1) { ░║ | ||
| 19 | +║ 8 println("Hello, " + g: name() + "! (" + i + ")") ░║ | ||
| 20 | +║ 9 } ▼║ | ||
| 21 | +║◄▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░►║ | ||
| 22 | +╚════════════════════════════════════════════════════════════════════════════════╝ | ||
| 23 | + F1 Describe F2 Save F3 Open F6 Window F7 Next F10 Menu 1:1 LSP: ready | ||
| 24 | +``` | ||
| 25 | + | ||
| 26 | +## Getting started | ||
| 27 | + | ||
| 28 | +```bash | ||
| 29 | +make install | ||
| 30 | +``` | ||
| 31 | + | ||
| 32 | +That builds the editor, puts it where your shell looks for commands, and reports what it found — the Go version it built with, where the binary went, whether that directory is on your `PATH`, and whether `golo` is installed. Then, from any directory of Golo scripts: | ||
| 33 | + | ||
| 34 | +```bash | ||
| 35 | +turbo-golo main.golo | ||
| 36 | +``` | ||
| 37 | + | ||
| 38 | +To build without installing, `make build` leaves the binary in `bin/turbo-golo`. From the module proxy instead of a checkout: `go install rickub.com/turbo-editors/turbo-golo@latest`. | ||
| 39 | + | ||
| 40 | +For completion and diagnostics, install GoloScript as well — the editor works without it, and says so on the status bar. The language server is the interpreter itself, in `golo lsp` mode, so there is nothing separate to install: a machine that can run Golo can complete Golo. Precompiled binaries are on [the release page](https://codeberg.org/TypeUnsafe/golo-script/releases); [the install guide](docs/en/how-to/install-goloscript.md) covers them, building from source, and how to check the editor really found it. | ||
| 41 | + | ||
| 42 | +The [tutorial](docs/en/tutorials/getting-started.md) walks through a first session in about ten minutes, and [`demos/`](demos/) holds three Golo programs to open in it — a small one, one with tests, and a tour of every construct the scanner colours. | ||
| 43 | + | ||
| 44 | +## Features | ||
| 45 | + | ||
| 46 | +- **Every build knows what it is** — `turbo-golo -version` and **Help ▸ About** name the version, the commit and the build date, stamped in by the linker from `git describe` rather than read from a constant somebody forgot to bump | ||
| 47 | +- **Turbo Vision interface** — menu bar with `Alt`-letter hot keys, overlapping movable and resizable windows, modal dialogs, mouse support throughout | ||
| 48 | +- **Syntax colouring for nine languages** — Golo by a hand-written scanner that follows GoloScript's lexer: `#` and `----` comments, strings carried to their closing quote wherever it is, `"""` multi-line strings, character literals, the `L` and `F` suffixes, the rule that keeps `1..3` an integer and a range, and names made of any Unicode letter or emoji. Plus TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell scripts from turbo-core | ||
| 49 | +- **Types by convention** — a capitalised name is a struct, a union or a variant, because that is how Golo writes them; `Some`, `None`, `Ok` and `Err` are types here and not constants, because in Golo they are variants of ordinary unions from `gololang.Errors` | ||
| 50 | +- **Themes** in TOML, eleven embedded — Borland navy, dark grey, paper white, espresso, Catppuccin Frappé and Latte, cobalt, Darcula and IntelliJ Light, and a hueless monochrome in both polarities — and any number of your own, with inheritance between files and between style keys. Every shipped theme is held to its contrast by tests | ||
| 51 | +- **Per-project settings** in `.turbo-golo/settings.toml` — pin a theme, turn on automatic saving — created from a menu item, never by itself, and re-read on every save so a change takes effect without a restart | ||
| 52 | +- **Completion, hover, go-to-definition, references, symbols and diagnostics** from `golo lsp`, entirely optional. The server is the interpreter, looked for on `PATH` and in `/usr/local/bin`, where GoloScript's installer puts it. It does not answer type definitions, and the editor says so rather than pretending | ||
| 53 | +- **Terminal windows** — `F8` opens a real shell in a window, with its own VT/ANSI emulator, scrollback and job control (Linux, macOS and Windows) | ||
| 54 | +- **Project tree** — `F9` shows the project's files in a window; walk it with the arrows and press Enter to open one | ||
| 55 | +- **Snippets** — a `Snippets` menu built from `.turbo-golo/snippets.toml`, grouped into submenus and filtered by the file you are in; the chosen text is inserted at the cursor and re-indented to match | ||
| 56 | +- **The GoloScript toolchain a menu away** — `Alt-G` runs the script, the tests, the debugger, the REPL, `golo new`, `gogolo build` and `wagolo build` from `.turbo-golo/tools.toml`, each showing its output where the tool asked: a popup that fills in as it goes, a terminal window, or an editing window to search. Golo has no manifest, so six of the eight commands ask which file they are for before they run — and a tool naming a `menu` of its own gets that menu on the bar | ||
| 57 | +- **Editing** with word movement, block indent, a shared clipboard, and undo that merges a run of typing into one step | ||
| 58 | +- **Faithful files** — line endings and the trailing newline are preserved, and saving is atomic | ||
| 59 | +- **Automatic saving**, off by default, writing a short while after you stop typing | ||
| 60 | + | ||
| 61 | +## Commands | ||
| 62 | + | ||
| 63 | +| Command | What it does | | ||
| 64 | +| --- | --- | | ||
| 65 | +| `make install` | Build and install onto your `PATH` | | ||
| 66 | +| `make build` | Compile into `bin/turbo-golo` | | ||
| 67 | +| `make test` | Run the whole test suite | | ||
| 68 | +| `make check` | `fmt`, `vet`, then the tests — what a commit should pass | | ||
| 69 | +| `make run FILE=x.golo` | Build and start the editor on a file | | ||
| 70 | +| `make help` | List every target | | ||
| 71 | + | ||
| 72 | +```bash | ||
| 73 | +turbo-golo [-theme name] [-no-lsp] [file...] | ||
| 74 | +turbo-golo -list-themes | ||
| 75 | +``` | ||
| 76 | + | ||
| 77 | +## Documentation | ||
| 78 | + | ||
| 79 | +Full documentation in **[English](docs/en/)** and **[French](docs/fr/)**, organised by the [Diátaxis](https://diataxis.fr) method: | ||
| 80 | + | ||
| 81 | +| | | | ||
| 82 | +| --- | --- | | ||
| 83 | +| **Tutorial** | [Your first Golo program in Turbo Golo](docs/en/tutorials/getting-started.md) | | ||
| 84 | +| **How-to** | [install the editor](docs/en/how-to/install.md) · [install GoloScript](docs/en/how-to/install-goloscript.md) · [run the tests](docs/en/how-to/run-the-tests.md) · [enable completion](docs/en/how-to/enable-completion.md) · [write a theme](docs/en/how-to/write-a-theme.md) · [move around a file](docs/en/how-to/navigate-code.md) · [ask about code](docs/en/how-to/ask-about-code.md) · [use a terminal](docs/en/how-to/use-a-terminal.md) · [configure a project](docs/en/how-to/configure-a-project.md) · [browse a project](docs/en/how-to/browse-a-project.md) · [use snippets](docs/en/how-to/use-snippets.md) · [run Golo commands](docs/en/how-to/run-golo-commands.md) · [make a release](docs/en/how-to/make-a-release.md) | | ||
| 85 | +| **Reference** | [command line](docs/en/reference/cli.md) · [keyboard](docs/en/reference/keyboard.md) · [menus](docs/en/reference/menus.md) · [theme format](docs/en/reference/themes.md) · [terminal windows](docs/en/reference/terminal.md) · [project settings](docs/en/reference/project-settings.md) · [project tree](docs/en/reference/project-tree.md) · [languages](docs/en/reference/languages.md) · [snippets](docs/en/reference/snippets.md) · [Golo tools](docs/en/reference/golo-tools.md) · [the version number](docs/en/reference/versioning.md) | | ||
| 86 | +| **Explanation** | [architecture](docs/en/explanation/architecture.md) · [design decisions](docs/en/explanation/design-decisions.md) · [colouring and completion](docs/en/explanation/colouring-and-completion.md) · [terminal windows](docs/en/explanation/terminal-windows.md) · [project settings](docs/en/explanation/project-settings.md) · [project tree](docs/en/explanation/project-tree.md) · [snippets](docs/en/explanation/snippets.md) · [Golo tools](docs/en/explanation/golo-tools.md) | | ||
| 87 | + | ||
| 88 | +The library's packages each carry their own `README.md` beside the code, in [turbo-core](https://rickub.com/turbo-editors/turbo-core). | ||
| 89 | + | ||
| 90 | +## Where the code is | ||
| 91 | + | ||
| 92 | +| | | | ||
| 93 | +| --- | --- | | ||
| 94 | +| `main.go` | flags, the terminal, the wiring | | ||
| 95 | +| `internal/gololang` | the profile, the Golo scanner, the three starter files | | ||
| 96 | +| `demos/` | three Golo programs to open in the editor; each runs under `golo` | | ||
| 97 | +| everything else | [turbo-core](https://rickub.com/turbo-editors/turbo-core) | | ||
| 98 | + | ||
| 99 | +The dependency graph is drawn in [`docs/diagrams/packages.drawio`](docs/diagrams/packages.drawio), checked against `go list` by `diagram_test.go`. | ||
| 100 | + | ||
| 101 | +## Design in one line | ||
| 102 | + | ||
| 103 | +Two dependencies — `tcell/v2` and `BurntSushi/toml` — and everything else from the standard library, including the scanner toolkit and the Language Server Protocol client. Both come through turbo-core; this repository adds none of its own. The [design decisions](docs/en/explanation/design-decisions.md) page explains why. | ||
| 104 | + | ||
| 105 | +## Requirements | ||
| 106 | + | ||
| 107 | +Go 1.26 or later to build it — the editor is written in Go even though it is an editor for Golo. A terminal with mouse reporting, which is all of them. GoloScript is optional, and is what completion, the error marks and the Golo menu's commands need. | ||
| 108 | + | ||
| 109 | +## Licence | ||
| 110 | + | ||
| 111 | +See [LICENSE](LICENSE). | ||
added
demos/.gitignore +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +golo-search | |
| \ No newline at end of file | ||
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +golo-search | ||
| \ No newline at end of file | \ No newline at end of file | ||
added
demos/.turbo-golo/acp.toml +14 -0 | new file mode 100644 | ||
| @@ -0,0 +1,14 @@ | ||
| 1 | +[[agent]] | |
| 2 | +name = "Bob (llama.cpp)" | |
| 3 | +command = "docker" | |
| 4 | +args = ["agent", "serve", "acp", ".turbo-golo/agent.yaml"] | |
| 5 | +env = { TELEMETRY_ENABLED = "false" } | |
| 6 | + | |
| 7 | +# The user's own agent, the one Zed runs as "mini-me": it speaks the protocol | |
| 8 | +# on stdin/stdout when started with -acp, and announces slash commands the | |
| 9 | +# editor lists when / is typed at the start of the box. | |
| 10 | +[[agent]] | |
| 11 | +name = "mini-me (llama.cpp)" | |
| 12 | +command = "mm" | |
| 13 | +args = ["-acp"] | |
| 14 | +env = { AGENT_CONFIG = "/Users/k33g/kDrive/Rickub/bots-garden/mini-me/agent.llamacpp.yaml" } | |
| new file mode 100644 | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | +[[agent]] | ||
| 2 | +name = "Bob (llama.cpp)" | ||
| 3 | +command = "docker" | ||
| 4 | +args = ["agent", "serve", "acp", ".turbo-golo/agent.yaml"] | ||
| 5 | +env = { TELEMETRY_ENABLED = "false" } | ||
| 6 | + | ||
| 7 | +# The user's own agent, the one Zed runs as "mini-me": it speaks the protocol | ||
| 8 | +# on stdin/stdout when started with -acp, and announces slash commands the | ||
| 9 | +# editor lists when / is typed at the start of the box. | ||
| 10 | +[[agent]] | ||
| 11 | +name = "mini-me (llama.cpp)" | ||
| 12 | +command = "mm" | ||
| 13 | +args = ["-acp"] | ||
| 14 | +env = { AGENT_CONFIG = "/Users/k33g/kDrive/Rickub/bots-garden/mini-me/agent.llamacpp.yaml" } | ||
added
demos/.turbo-golo/agent.yaml +29 -0 | new file mode 100644 | ||
| @@ -0,0 +1,29 @@ | ||
| 1 | +# /Users/k33g/CodeBerg/turbo-editors/turbo-go/acp-agent/agent.yaml | |
| 2 | +providers: | |
| 3 | + llamacpp: | |
| 4 | + api_type: openai_chatcompletions | |
| 5 | + base_url: http://host.docker.internal:8080/v1 | |
| 6 | + | |
| 7 | +models: | |
| 8 | + mellum2: | |
| 9 | + provider: llamacpp | |
| 10 | + model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M | |
| 11 | + #max_tokens: 8192 | |
| 12 | + temperature: 0.7 | |
| 13 | + provider_opts: | |
| 14 | + context_size: 262144 | |
| 15 | + | |
| 16 | +agents: | |
| 17 | + root: | |
| 18 | + model: mellum2 | |
| 19 | + description: A helpful AI assistant running on a local llama.cpp server | |
| 20 | + instruction: | | |
| 21 | + You name is Bob 🤓, you are a knowledgeable code assistant that helps users with various tasks. | |
| 22 | + Be helpful, accurate, and concise in your responses. | |
| 23 | + You have access to the local filesystem and shell: use these tools | |
| 24 | + welcome_message: | | |
| 25 | + 🤖 Local Assistant propulsed by **llama.cpp** 🦙 | |
| 26 | + | |
| 27 | + toolsets: | |
| 28 | + - type: filesystem | |
| 29 | + - type: shell | |
| new file mode 100644 | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | +# /Users/k33g/CodeBerg/turbo-editors/turbo-go/acp-agent/agent.yaml | ||
| 2 | +providers: | ||
| 3 | + llamacpp: | ||
| 4 | + api_type: openai_chatcompletions | ||
| 5 | + base_url: http://host.docker.internal:8080/v1 | ||
| 6 | + | ||
| 7 | +models: | ||
| 8 | + mellum2: | ||
| 9 | + provider: llamacpp | ||
| 10 | + model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M | ||
| 11 | + #max_tokens: 8192 | ||
| 12 | + temperature: 0.7 | ||
| 13 | + provider_opts: | ||
| 14 | + context_size: 262144 | ||
| 15 | + | ||
| 16 | +agents: | ||
| 17 | + root: | ||
| 18 | + model: mellum2 | ||
| 19 | + description: A helpful AI assistant running on a local llama.cpp server | ||
| 20 | + instruction: | | ||
| 21 | + You name is Bob 🤓, you are a knowledgeable code assistant that helps users with various tasks. | ||
| 22 | + Be helpful, accurate, and concise in your responses. | ||
| 23 | + You have access to the local filesystem and shell: use these tools | ||
| 24 | + welcome_message: | | ||
| 25 | + 🤖 Local Assistant propulsed by **llama.cpp** 🦙 | ||
| 26 | + | ||
| 27 | + toolsets: | ||
| 28 | + - type: filesystem | ||
| 29 | + - type: shell | ||
added
demos/.turbo-golo/settings.toml +18 -0 | new file mode 100644 | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +# turbo-golo project settings. | |
| 2 | +# | |
| 3 | +# These apply to everyone who opens this project in turbo-golo. Delete this | |
| 4 | +# file and the editor falls back to its own defaults. | |
| 5 | + | |
| 6 | +[editor] | |
| 7 | + | |
| 8 | +# The colour theme to start in. `turbo-golo -list-themes` lists them all. | |
| 9 | +# A -theme flag on the command line overrides this. | |
| 10 | +theme = "intellij-light" | |
| 11 | + | |
| 12 | +# Write modified files by themselves, a short while after you stop typing. | |
| 13 | +# On, because a project that has gone to the trouble of having a settings file | |
| 14 | +# has said what it wants; set it to false and save, and it stops at once. | |
| 15 | +autosave = true | |
| 16 | + | |
| 17 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | |
| 18 | +autosave_delay = "2s" | |
| new file mode 100644 | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | +# turbo-golo project settings. | ||
| 2 | +# | ||
| 3 | +# These apply to everyone who opens this project in turbo-golo. Delete this | ||
| 4 | +# file and the editor falls back to its own defaults. | ||
| 5 | + | ||
| 6 | +[editor] | ||
| 7 | + | ||
| 8 | +# The colour theme to start in. `turbo-golo -list-themes` lists them all. | ||
| 9 | +# A -theme flag on the command line overrides this. | ||
| 10 | +theme = "intellij-light" | ||
| 11 | + | ||
| 12 | +# Write modified files by themselves, a short while after you stop typing. | ||
| 13 | +# On, because a project that has gone to the trouble of having a settings file | ||
| 14 | +# has said what it wants; set it to false and save, and it stops at once. | ||
| 15 | +autosave = true | ||
| 16 | + | ||
| 17 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | ||
| 18 | +autosave_delay = "2s" | ||
added
demos/.turbo-golo/snippets.toml +145 -0 | new file mode 100644 | ||
| @@ -0,0 +1,145 @@ | ||
| 1 | +# turbo-golo snippets. | |
| 2 | +# | |
| 3 | +# Each [[snippet]] becomes one line of the Snippets menu. Snippets sharing a | |
| 4 | +# group appear together in a submenu of that name; one with no group goes into | |
| 5 | +# General. A snippet is inserted at the cursor, and every line after the | |
| 6 | +# first is indented to match the line you inserted it on. | |
| 7 | +# | |
| 8 | +# languages restricts a snippet to files of those kinds, by the names the | |
| 9 | +# editor uses: bash, dockerfile, golo, html, javascript, markdown, toml, xml, | |
| 10 | +# yaml. Leave it out and the snippet is offered everywhere. | |
| 11 | +# | |
| 12 | +# Bodies are indented with two spaces, which is what every example in the | |
| 13 | +# GoloScript documentation and its own templates use. Golo has no formatter to | |
| 14 | +# disagree with, so the convention is the only authority there is. | |
| 15 | +# | |
| 16 | +# Every Golo body below is written in single quotes — '''…''' rather than | |
| 17 | +# """…""" — because a Golo string carries \n and \" the way a Go string does, | |
| 18 | +# and TOML would interpret those escapes in a basic string before the editor | |
| 19 | +# ever saw them. In a literal string a backslash is just a backslash, which is | |
| 20 | +# what a Golo snippet needs. | |
| 21 | +# | |
| 22 | +# Your own snippets, shared across every project, go in: | |
| 23 | +# /Users/k33g/Library/Application Support/turbo-golo/snippets.toml | |
| 24 | + | |
| 25 | +[[snippet]] | |
| 26 | +name = "module" | |
| 27 | +group = "Golo" | |
| 28 | +languages = ["golo"] | |
| 29 | +body = ''' | |
| 30 | +module hello.World | |
| 31 | + | |
| 32 | +function main = |args| { | |
| 33 | + println("Hello, Golo!") | |
| 34 | +}''' | |
| 35 | + | |
| 36 | +[[snippet]] | |
| 37 | +name = "main" | |
| 38 | +group = "Golo" | |
| 39 | +languages = ["golo"] | |
| 40 | +body = ''' | |
| 41 | +function main = |args| { | |
| 42 | + println("Hello, Golo!") | |
| 43 | +}''' | |
| 44 | + | |
| 45 | +[[snippet]] | |
| 46 | +name = "function" | |
| 47 | +group = "Golo" | |
| 48 | +languages = ["golo"] | |
| 49 | +body = ''' | |
| 50 | +function name = |a, b| { | |
| 51 | + return a + b | |
| 52 | +}''' | |
| 53 | + | |
| 54 | +[[snippet]] | |
| 55 | +name = "closure" | |
| 56 | +group = "Golo" | |
| 57 | +languages = ["golo"] | |
| 58 | +body = ''' | |
| 59 | +let f = |x| -> x * 2''' | |
| 60 | + | |
| 61 | +[[snippet]] | |
| 62 | +name = "struct" | |
| 63 | +group = "Golo" | |
| 64 | +languages = ["golo"] | |
| 65 | +body = ''' | |
| 66 | +struct Point = { x, y }''' | |
| 67 | + | |
| 68 | +[[snippet]] | |
| 69 | +name = "union" | |
| 70 | +group = "Golo" | |
| 71 | +languages = ["golo"] | |
| 72 | +body = ''' | |
| 73 | +union Shape = { | |
| 74 | + Circle = { radius } | |
| 75 | + Rect = { width, height } | |
| 76 | +}''' | |
| 77 | + | |
| 78 | +[[snippet]] | |
| 79 | +name = "augment" | |
| 80 | +group = "Golo" | |
| 81 | +languages = ["golo"] | |
| 82 | +body = ''' | |
| 83 | +augment Point { | |
| 84 | + function describe = |this| -> "(" + this: x() + ", " + this: y() + ")" | |
| 85 | +}''' | |
| 86 | + | |
| 87 | +[[snippet]] | |
| 88 | +name = "match" | |
| 89 | +group = "Golo" | |
| 90 | +languages = ["golo"] | |
| 91 | +body = ''' | |
| 92 | +let label = match { | |
| 93 | + when n < 0 then "negative" | |
| 94 | + when n == 0 then "zero" | |
| 95 | + otherwise "positive" | |
| 96 | +}''' | |
| 97 | + | |
| 98 | +[[snippet]] | |
| 99 | +name = "foreach" | |
| 100 | +group = "Golo" | |
| 101 | +languages = ["golo"] | |
| 102 | +body = ''' | |
| 103 | +foreach item in list[1, 2, 3] { | |
| 104 | + println(item) | |
| 105 | +}''' | |
| 106 | + | |
| 107 | +[[snippet]] | |
| 108 | +name = "for" | |
| 109 | +group = "Golo" | |
| 110 | +languages = ["golo"] | |
| 111 | +body = ''' | |
| 112 | +for (var i = 0, i < 10, i = i + 1) { | |
| 113 | + println(i) | |
| 114 | +}''' | |
| 115 | + | |
| 116 | +[[snippet]] | |
| 117 | +name = "try" | |
| 118 | +group = "Golo" | |
| 119 | +languages = ["golo"] | |
| 120 | +body = ''' | |
| 121 | +try { | |
| 122 | + throw "boom" | |
| 123 | +} catch (e) { | |
| 124 | + println("caught: \"" + e + "\"") | |
| 125 | +} finally { | |
| 126 | + println("done") | |
| 127 | +}''' | |
| 128 | + | |
| 129 | +[[snippet]] | |
| 130 | +name = "comprehension" | |
| 131 | +group = "Golo" | |
| 132 | +languages = ["golo"] | |
| 133 | +body = ''' | |
| 134 | +let squares = list[x * x foreach x in range(1, 6) when x > 2]''' | |
| 135 | + | |
| 136 | +[[snippet]] | |
| 137 | +group = "General" | |
| 138 | +name = "Hello" | |
| 139 | +body = "Hello!!!" | |
| 140 | + | |
| 141 | +[[snippet]] | |
| 142 | +group = "Markdown" | |
| 143 | +name = "Image" | |
| 144 | +languages = ["markdown"] | |
| 145 | +body = "" | |
| new file mode 100644 | |||
| @@ -0,0 +1,145 @@ | |||
| 1 | +# turbo-golo snippets. | ||
| 2 | +# | ||
| 3 | +# Each [[snippet]] becomes one line of the Snippets menu. Snippets sharing a | ||
| 4 | +# group appear together in a submenu of that name; one with no group goes into | ||
| 5 | +# General. A snippet is inserted at the cursor, and every line after the | ||
| 6 | +# first is indented to match the line you inserted it on. | ||
| 7 | +# | ||
| 8 | +# languages restricts a snippet to files of those kinds, by the names the | ||
| 9 | +# editor uses: bash, dockerfile, golo, html, javascript, markdown, toml, xml, | ||
| 10 | +# yaml. Leave it out and the snippet is offered everywhere. | ||
| 11 | +# | ||
| 12 | +# Bodies are indented with two spaces, which is what every example in the | ||
| 13 | +# GoloScript documentation and its own templates use. Golo has no formatter to | ||
| 14 | +# disagree with, so the convention is the only authority there is. | ||
| 15 | +# | ||
| 16 | +# Every Golo body below is written in single quotes — '''…''' rather than | ||
| 17 | +# """…""" — because a Golo string carries \n and \" the way a Go string does, | ||
| 18 | +# and TOML would interpret those escapes in a basic string before the editor | ||
| 19 | +# ever saw them. In a literal string a backslash is just a backslash, which is | ||
| 20 | +# what a Golo snippet needs. | ||
| 21 | +# | ||
| 22 | +# Your own snippets, shared across every project, go in: | ||
| 23 | +# /Users/k33g/Library/Application Support/turbo-golo/snippets.toml | ||
| 24 | + | ||
| 25 | +[[snippet]] | ||
| 26 | +name = "module" | ||
| 27 | +group = "Golo" | ||
| 28 | +languages = ["golo"] | ||
| 29 | +body = ''' | ||
| 30 | +module hello.World | ||
| 31 | + | ||
| 32 | +function main = |args| { | ||
| 33 | + println("Hello, Golo!") | ||
| 34 | +}''' | ||
| 35 | + | ||
| 36 | +[[snippet]] | ||
| 37 | +name = "main" | ||
| 38 | +group = "Golo" | ||
| 39 | +languages = ["golo"] | ||
| 40 | +body = ''' | ||
| 41 | +function main = |args| { | ||
| 42 | + println("Hello, Golo!") | ||
| 43 | +}''' | ||
| 44 | + | ||
| 45 | +[[snippet]] | ||
| 46 | +name = "function" | ||
| 47 | +group = "Golo" | ||
| 48 | +languages = ["golo"] | ||
| 49 | +body = ''' | ||
| 50 | +function name = |a, b| { | ||
| 51 | + return a + b | ||
| 52 | +}''' | ||
| 53 | + | ||
| 54 | +[[snippet]] | ||
| 55 | +name = "closure" | ||
| 56 | +group = "Golo" | ||
| 57 | +languages = ["golo"] | ||
| 58 | +body = ''' | ||
| 59 | +let f = |x| -> x * 2''' | ||
| 60 | + | ||
| 61 | +[[snippet]] | ||
| 62 | +name = "struct" | ||
| 63 | +group = "Golo" | ||
| 64 | +languages = ["golo"] | ||
| 65 | +body = ''' | ||
| 66 | +struct Point = { x, y }''' | ||
| 67 | + | ||
| 68 | +[[snippet]] | ||
| 69 | +name = "union" | ||
| 70 | +group = "Golo" | ||
| 71 | +languages = ["golo"] | ||
| 72 | +body = ''' | ||
| 73 | +union Shape = { | ||
| 74 | + Circle = { radius } | ||
| 75 | + Rect = { width, height } | ||
| 76 | +}''' | ||
| 77 | + | ||
| 78 | +[[snippet]] | ||
| 79 | +name = "augment" | ||
| 80 | +group = "Golo" | ||
| 81 | +languages = ["golo"] | ||
| 82 | +body = ''' | ||
| 83 | +augment Point { | ||
| 84 | + function describe = |this| -> "(" + this: x() + ", " + this: y() + ")" | ||
| 85 | +}''' | ||
| 86 | + | ||
| 87 | +[[snippet]] | ||
| 88 | +name = "match" | ||
| 89 | +group = "Golo" | ||
| 90 | +languages = ["golo"] | ||
| 91 | +body = ''' | ||
| 92 | +let label = match { | ||
| 93 | + when n < 0 then "negative" | ||
| 94 | + when n == 0 then "zero" | ||
| 95 | + otherwise "positive" | ||
| 96 | +}''' | ||
| 97 | + | ||
| 98 | +[[snippet]] | ||
| 99 | +name = "foreach" | ||
| 100 | +group = "Golo" | ||
| 101 | +languages = ["golo"] | ||
| 102 | +body = ''' | ||
| 103 | +foreach item in list[1, 2, 3] { | ||
| 104 | + println(item) | ||
| 105 | +}''' | ||
| 106 | + | ||
| 107 | +[[snippet]] | ||
| 108 | +name = "for" | ||
| 109 | +group = "Golo" | ||
| 110 | +languages = ["golo"] | ||
| 111 | +body = ''' | ||
| 112 | +for (var i = 0, i < 10, i = i + 1) { | ||
| 113 | + println(i) | ||
| 114 | +}''' | ||
| 115 | + | ||
| 116 | +[[snippet]] | ||
| 117 | +name = "try" | ||
| 118 | +group = "Golo" | ||
| 119 | +languages = ["golo"] | ||
| 120 | +body = ''' | ||
| 121 | +try { | ||
| 122 | + throw "boom" | ||
| 123 | +} catch (e) { | ||
| 124 | + println("caught: \"" + e + "\"") | ||
| 125 | +} finally { | ||
| 126 | + println("done") | ||
| 127 | +}''' | ||
| 128 | + | ||
| 129 | +[[snippet]] | ||
| 130 | +name = "comprehension" | ||
| 131 | +group = "Golo" | ||
| 132 | +languages = ["golo"] | ||
| 133 | +body = ''' | ||
| 134 | +let squares = list[x * x foreach x in range(1, 6) when x > 2]''' | ||
| 135 | + | ||
| 136 | +[[snippet]] | ||
| 137 | +group = "General" | ||
| 138 | +name = "Hello" | ||
| 139 | +body = "Hello!!!" | ||
| 140 | + | ||
| 141 | +[[snippet]] | ||
| 142 | +group = "Markdown" | ||
| 143 | +name = "Image" | ||
| 144 | +languages = ["markdown"] | ||
| 145 | +body = "" | ||
added
demos/.turbo-golo/tools.toml +109 -0 | new file mode 100644 | ||
| @@ -0,0 +1,109 @@ | ||
| 1 | +# turbo-golo tools. | |
| 2 | +# | |
| 3 | +# Each [[tool]] becomes one line of the Golo menu, in the order they appear | |
| 4 | +# here. name is what the menu shows; a letter between tildes is its hot key, and | |
| 5 | +# no two tools should claim the same one. | |
| 6 | +# | |
| 7 | +# command goes to "sh -c", so pipes, globs and && work: one entry can be a | |
| 8 | +# whole sequence. | |
| 9 | +# | |
| 10 | +# menu says which menu it appears in. Leave it out and the tool goes into the | |
| 11 | +# Golo menu; name anything else and that menu is created for you, in the order | |
| 12 | +# the names first appear here. A tool that has nothing to do with Golo belongs | |
| 13 | +# in one of your own: | |
| 14 | +# | |
| 15 | +# [[tool]] | |
| 16 | +# name = "~E~cho" | |
| 17 | +# command = "echo TADA" | |
| 18 | +# menu = "Tools" | |
| 19 | +# | |
| 20 | +# A {{label}} in a command is a value the editor asks for before running it, in | |
| 21 | +# a box titled after the tool. The text between the braces is what it asks for: | |
| 22 | +# | |
| 23 | +# [[tool]] | |
| 24 | +# name = "~R~un" | |
| 25 | +# command = "golo {{script}}" | |
| 26 | +# | |
| 27 | +# The value is quoted, so a path with a space in it stays one argument. Add ... | |
| 28 | +# inside the braces when you mean several arguments rather than one value: | |
| 29 | +# | |
| 30 | +# [[tool]] | |
| 31 | +# name = "Run with ~a~rguments" | |
| 32 | +# command = "golo main.golo {{arguments...}}" | |
| 33 | +# | |
| 34 | +# Double braces, not single. Single ones appear in real commands — awk '{print | |
| 35 | +# $1}' and find . -exec rm {} + are both ordinary things to put here — and | |
| 36 | +# neither is asking you for anything. | |
| 37 | +# | |
| 38 | +# output says where what the command prints goes: | |
| 39 | +# popup a dialog that fills in as it runs, and says the exit code (default) | |
| 40 | +# terminal a terminal window, for anything that reads the keyboard or runs long | |
| 41 | +# editor an editing window once it has finished, to search with Ctrl-F | |
| 42 | +# | |
| 43 | +# Commands run in the directory the editor was started in, which is why they | |
| 44 | +# see the whole project when you start from its root. Golo has no project | |
| 45 | +# manifest: a script is a file, and every command here names the file it | |
| 46 | +# works on. | |
| 47 | + | |
| 48 | +[[tool]] | |
| 49 | +name = "~R~un" | |
| 50 | +# The interpreter, on the file you name. A terminal, not a popup: a script that | |
| 51 | +# reads the keyboard has to be able to be answered, and one that runs long has | |
| 52 | +# to be able to be interrupted. | |
| 53 | +command = "golo {{script, e.g. main.golo}}" | |
| 54 | +output = "terminal" | |
| 55 | + | |
| 56 | +[[tool]] | |
| 57 | +name = "~T~est" | |
| 58 | +# Every *_test.golo under the current directory, with gololang.Testing. | |
| 59 | +command = "golo --test" | |
| 60 | +output = "popup" | |
| 61 | + | |
| 62 | +[[tool]] | |
| 63 | +name = "Test ~o~ne" | |
| 64 | +command = "golo --test {{test file or directory}}" | |
| 65 | +output = "popup" | |
| 66 | + | |
| 67 | +[[tool]] | |
| 68 | +name = "~D~ebug" | |
| 69 | +# The same interpreter with its step debugger on. It reads the keyboard, so it | |
| 70 | +# needs a terminal. | |
| 71 | +command = "golo --debug {{script, e.g. main.golo}}" | |
| 72 | +output = "terminal" | |
| 73 | + | |
| 74 | +[[tool]] | |
| 75 | +name = "R~E~PL" | |
| 76 | +# golo with no file starts its read-eval-print loop. | |
| 77 | +command = "golo" | |
| 78 | +output = "terminal" | |
| 79 | + | |
| 80 | +[[tool]] | |
| 81 | +name = "~N~ew script" | |
| 82 | +# Writes a starter program from GoloScript's own template. | |
| 83 | +command = "golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}" | |
| 84 | +output = "popup" | |
| 85 | + | |
| 86 | +[[tool]] | |
| 87 | +name = "~B~uild native" | |
| 88 | +# gogolo transpiles the script to Go and compiles it to a native executable. | |
| 89 | +# It needs the Go toolchain on PATH. | |
| 90 | +command = "gogolo build -o {{output executable}} {{script, e.g. main.golo}}" | |
| 91 | +output = "popup" | |
| 92 | + | |
| 93 | +[[tool]] | |
| 94 | +name = "Build ~w~asm" | |
| 95 | +# wagolo transpiles the script to Go and compiles it to WebAssembly with | |
| 96 | +# TinyGo. wasi is the target a runtime such as wasmtime or Node runs; js and | |
| 97 | +# wasip2 are the others. | |
| 98 | +command = "wagolo build -target={{target: wasi, js or wasip2}} -o {{output .wasm}} {{script, e.g. main.golo}}" | |
| 99 | +output = "popup" | |
| 100 | + | |
| 101 | +# A tool naming a `menu` gets a menu of its own on the bar. Nothing above does, | |
| 102 | +# so every tool above is in the Golo menu. This one is in a menu called Tools, | |
| 103 | +# which appears between Golo and Help — that is the whole mechanism. | |
| 104 | + | |
| 105 | +[[tool]] | |
| 106 | +name = "~E~cho" | |
| 107 | +command = "echo 🎉 tada!" | |
| 108 | +menu = "Tools" | |
| 109 | +output = "terminal" | |
| new file mode 100644 | |||
| @@ -0,0 +1,109 @@ | |||
| 1 | +# turbo-golo tools. | ||
| 2 | +# | ||
| 3 | +# Each [[tool]] becomes one line of the Golo menu, in the order they appear | ||
| 4 | +# here. name is what the menu shows; a letter between tildes is its hot key, and | ||
| 5 | +# no two tools should claim the same one. | ||
| 6 | +# | ||
| 7 | +# command goes to "sh -c", so pipes, globs and && work: one entry can be a | ||
| 8 | +# whole sequence. | ||
| 9 | +# | ||
| 10 | +# menu says which menu it appears in. Leave it out and the tool goes into the | ||
| 11 | +# Golo menu; name anything else and that menu is created for you, in the order | ||
| 12 | +# the names first appear here. A tool that has nothing to do with Golo belongs | ||
| 13 | +# in one of your own: | ||
| 14 | +# | ||
| 15 | +# [[tool]] | ||
| 16 | +# name = "~E~cho" | ||
| 17 | +# command = "echo TADA" | ||
| 18 | +# menu = "Tools" | ||
| 19 | +# | ||
| 20 | +# A {{label}} in a command is a value the editor asks for before running it, in | ||
| 21 | +# a box titled after the tool. The text between the braces is what it asks for: | ||
| 22 | +# | ||
| 23 | +# [[tool]] | ||
| 24 | +# name = "~R~un" | ||
| 25 | +# command = "golo {{script}}" | ||
| 26 | +# | ||
| 27 | +# The value is quoted, so a path with a space in it stays one argument. Add ... | ||
| 28 | +# inside the braces when you mean several arguments rather than one value: | ||
| 29 | +# | ||
| 30 | +# [[tool]] | ||
| 31 | +# name = "Run with ~a~rguments" | ||
| 32 | +# command = "golo main.golo {{arguments...}}" | ||
| 33 | +# | ||
| 34 | +# Double braces, not single. Single ones appear in real commands — awk '{print | ||
| 35 | +# $1}' and find . -exec rm {} + are both ordinary things to put here — and | ||
| 36 | +# neither is asking you for anything. | ||
| 37 | +# | ||
| 38 | +# output says where what the command prints goes: | ||
| 39 | +# popup a dialog that fills in as it runs, and says the exit code (default) | ||
| 40 | +# terminal a terminal window, for anything that reads the keyboard or runs long | ||
| 41 | +# editor an editing window once it has finished, to search with Ctrl-F | ||
| 42 | +# | ||
| 43 | +# Commands run in the directory the editor was started in, which is why they | ||
| 44 | +# see the whole project when you start from its root. Golo has no project | ||
| 45 | +# manifest: a script is a file, and every command here names the file it | ||
| 46 | +# works on. | ||
| 47 | + | ||
| 48 | +[[tool]] | ||
| 49 | +name = "~R~un" | ||
| 50 | +# The interpreter, on the file you name. A terminal, not a popup: a script that | ||
| 51 | +# reads the keyboard has to be able to be answered, and one that runs long has | ||
| 52 | +# to be able to be interrupted. | ||
| 53 | +command = "golo {{script, e.g. main.golo}}" | ||
| 54 | +output = "terminal" | ||
| 55 | + | ||
| 56 | +[[tool]] | ||
| 57 | +name = "~T~est" | ||
| 58 | +# Every *_test.golo under the current directory, with gololang.Testing. | ||
| 59 | +command = "golo --test" | ||
| 60 | +output = "popup" | ||
| 61 | + | ||
| 62 | +[[tool]] | ||
| 63 | +name = "Test ~o~ne" | ||
| 64 | +command = "golo --test {{test file or directory}}" | ||
| 65 | +output = "popup" | ||
| 66 | + | ||
| 67 | +[[tool]] | ||
| 68 | +name = "~D~ebug" | ||
| 69 | +# The same interpreter with its step debugger on. It reads the keyboard, so it | ||
| 70 | +# needs a terminal. | ||
| 71 | +command = "golo --debug {{script, e.g. main.golo}}" | ||
| 72 | +output = "terminal" | ||
| 73 | + | ||
| 74 | +[[tool]] | ||
| 75 | +name = "R~E~PL" | ||
| 76 | +# golo with no file starts its read-eval-print loop. | ||
| 77 | +command = "golo" | ||
| 78 | +output = "terminal" | ||
| 79 | + | ||
| 80 | +[[tool]] | ||
| 81 | +name = "~N~ew script" | ||
| 82 | +# Writes a starter program from GoloScript's own template. | ||
| 83 | +command = "golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}" | ||
| 84 | +output = "popup" | ||
| 85 | + | ||
| 86 | +[[tool]] | ||
| 87 | +name = "~B~uild native" | ||
| 88 | +# gogolo transpiles the script to Go and compiles it to a native executable. | ||
| 89 | +# It needs the Go toolchain on PATH. | ||
| 90 | +command = "gogolo build -o {{output executable}} {{script, e.g. main.golo}}" | ||
| 91 | +output = "popup" | ||
| 92 | + | ||
| 93 | +[[tool]] | ||
| 94 | +name = "Build ~w~asm" | ||
| 95 | +# wagolo transpiles the script to Go and compiles it to WebAssembly with | ||
| 96 | +# TinyGo. wasi is the target a runtime such as wasmtime or Node runs; js and | ||
| 97 | +# wasip2 are the others. | ||
| 98 | +command = "wagolo build -target={{target: wasi, js or wasip2}} -o {{output .wasm}} {{script, e.g. main.golo}}" | ||
| 99 | +output = "popup" | ||
| 100 | + | ||
| 101 | +# A tool naming a `menu` gets a menu of its own on the bar. Nothing above does, | ||
| 102 | +# so every tool above is in the Golo menu. This one is in a menu called Tools, | ||
| 103 | +# which appears between Golo and Help — that is the whole mechanism. | ||
| 104 | + | ||
| 105 | +[[tool]] | ||
| 106 | +name = "~E~cho" | ||
| 107 | +command = "echo 🎉 tada!" | ||
| 108 | +menu = "Tools" | ||
| 109 | +output = "terminal" | ||
added
demos/README.md +17 -0 | new file mode 100644 | ||
| @@ -0,0 +1,17 @@ | ||
| 1 | +# Demos | |
| 2 | + | |
| 3 | +Three Golo programs to open in Turbo Golo. Each runs under `golo`, and the tests pass under `golo --test`: | |
| 4 | + | |
| 5 | +| Directory | What it shows | Run it | | |
| 6 | +| --- | --- | --- | | |
| 7 | +| `hello/` | The smallest program: a module, a function, a greeting | `golo demos/hello/hello.golo` | | |
| 8 | +| `shapes/` | Structs, a union with two variants, augmentations, closures and a comprehension — with a `*_test.golo` beside it | `golo demos/shapes/shapes.golo` then `golo --test demos/shapes` | | |
| 9 | +| `syntax-tour/` | One of everything the scanner colours, each line of `tour.golo` running; `lexer-only.golo` beside it holds the tokens the lexer reads and the parser refuses, coloured but not runnable | `golo demos/syntax-tour/tour.golo` | | |
| 10 | + | |
| 11 | +From the repository root: | |
| 12 | + | |
| 13 | +```bash | |
| 14 | +turbo-golo demos/shapes/shapes.golo | |
| 15 | +``` | |
| 16 | + | |
| 17 | +Open one, press `F9` for the project tree, and `Alt-G` for the Golo menu. Create the project's tools file from that menu and **Run** asks which script to run. | |
| new file mode 100644 | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | +# Demos | ||
| 2 | + | ||
| 3 | +Three Golo programs to open in Turbo Golo. Each runs under `golo`, and the tests pass under `golo --test`: | ||
| 4 | + | ||
| 5 | +| Directory | What it shows | Run it | | ||
| 6 | +| --- | --- | --- | | ||
| 7 | +| `hello/` | The smallest program: a module, a function, a greeting | `golo demos/hello/hello.golo` | | ||
| 8 | +| `shapes/` | Structs, a union with two variants, augmentations, closures and a comprehension — with a `*_test.golo` beside it | `golo demos/shapes/shapes.golo` then `golo --test demos/shapes` | | ||
| 9 | +| `syntax-tour/` | One of everything the scanner colours, each line of `tour.golo` running; `lexer-only.golo` beside it holds the tokens the lexer reads and the parser refuses, coloured but not runnable | `golo demos/syntax-tour/tour.golo` | | ||
| 10 | + | ||
| 11 | +From the repository root: | ||
| 12 | + | ||
| 13 | +```bash | ||
| 14 | +turbo-golo demos/shapes/shapes.golo | ||
| 15 | +``` | ||
| 16 | + | ||
| 17 | +Open one, press `F9` for the project tree, and `Alt-G` for the Golo menu. Create the project's tools file from that menu and **Run** asks which script to run. | ||
added
demos/hello-golo/.mm/sessions/20260916-052534-64c5f107.json +256 -0 | new file mode 100644 | ||
| @@ -0,0 +1,256 @@ | ||
| 1 | +{ | |
| 2 | + "id": "20260916-052534-64c5f107", | |
| 3 | + "cwd": "/Users/k33g/CodeBerg/turbo-editors/turbo-golo/demos/hello-golo", | |
| 4 | + "createdAt": "2026-09-16T05:25:34.343495Z", | |
| 5 | + "updatedAt": "2026-09-16T05:26:36.412951Z", | |
| 6 | + "messages": [ | |
| 7 | + { | |
| 8 | + "content": [ | |
| 9 | + { | |
| 10 | + "text": "Your name is Bob.\nYou are a coding agent working in a terminal.\nYou have a \"bash\" tool to run shell commands.\nUse it to explore files, run tests, inspect the repository, etc.\nChain several commands if needed, then answer clearly in English.\n\nA request often mixes things you answer from yourself (\"say hello\") with\nthings only a command can answer (\"list the files\"). Handle every part, in\nthe order asked, and run a command for each part that needs one.\nNever state the contents of a file, the output of a command, or the state of\nthe repository unless a command in THIS answer returned it. What you did not\nread, you do not know: run the command instead of recalling it.\n\nSKILLS\nYou have a second tool, `read_skill`. Its description lists the procedures\navailable for this project — one per kind of task.\n\nAny request to DO something to a Go project is a skill, not a shell command\nyou invent. Match the request against that list, call `read_skill` FIRST,\nbefore any bash command, and then follow what it says step by step.\n\nFILE EDITING\nYou have three tools for files: `read_file`, `edit_file` and `write_file`.\nThey are how a file gets read and changed here: each change is exact,\nchecked before it is written, and comes back as a diff with line numbers.\nbash is for running things — building, testing, listing, searching.\n\n- Read before you write: call `read_file` on the file (numbered=true when\n you need line numbers). You cannot target text you have not seen; never\n rely on what you think you remember about a file.\n- To change an existing file, call `edit_file` with one or more {old, new}\n pairs. `old` is copied from the file character for character — same\n spaces, same indentation, same line breaks — and appears exactly once:\n add the surrounding lines until it is unique. Several pairs are applied\n together, against the original file. An empty `new` deletes the text.\n- Call `write_file` only to create a file, or to rewrite one entirely and\n on purpose. On an existing file it replaces everything, including what\n you did not intend to touch.\n- Read the diff the tool returns: it says exactly what changed and on which\n line. If `edit_file` refuses — text not found, ambiguous, overlapping\n edits — read the file again and fix `old`. Do not fall back to\n `write_file` to force the change through.\n- After editing code, run the narrowest check with bash: the formatter, the\n compiler, or the test covering that file.\n\nRULES\n- Keep everything the file already does, unless the user asked to remove it.\n- Touch only the files the request is about. Do not add tests, files or\n features that were not asked for.\n- Never run a git command unless the user says git, commit or push.\n- Never move, rename or delete a file unless the user asked for it.\n- Then answer in English, in a few lines.\n- If you don't know how to use a \u003ccli\u003e, run `\u003ccli\u003e --help` (or `\u003ccli\u003e help`)\n to understand the options, then run the command.\n\nBACKGROUND JOBS\nNever let a command block the answer. Anything that serves, watches or runs\nlong goes to the background, with BOTH streams redirected and its pid kept:\n\n nohup \u003ccommand\u003e \u003e /tmp/\u003cjob\u003e.log 2\u003e\u00261 \u0026 echo $! \u003e /tmp/\u003cjob\u003e.pid\n\nRedirecting only stdout still blocks until the process exits. Read the\n`bg-jobs` skill before you wait on, inspect or stop such a job — each has a\nrule you cannot guess. Stop every job you started before you finish, and say\nwhich ones you left running.\n" | |
| 11 | + } | |
| 12 | + ], | |
| 13 | + "role": "system" | |
| 14 | + }, | |
| 15 | + { | |
| 16 | + "content": [ | |
| 17 | + { | |
| 18 | + "text": "do you know golo?" | |
| 19 | + } | |
| 20 | + ], | |
| 21 | + "role": "user" | |
| 22 | + }, | |
| 23 | + { | |
| 24 | + "content": [ | |
| 25 | + { | |
| 26 | + "toolRequest": { | |
| 27 | + "input": { | |
| 28 | + "command": "which golo" | |
| 29 | + }, | |
| 30 | + "name": "bash", | |
| 31 | + "ref": "LlxPQH40AEgDIYIMc4s4WxRfhPJMEYcA" | |
| 32 | + } | |
| 33 | + } | |
| 34 | + ], | |
| 35 | + "role": "model" | |
| 36 | + }, | |
| 37 | + { | |
| 38 | + "content": [ | |
| 39 | + { | |
| 40 | + "toolResponse": { | |
| 41 | + "name": "bash", | |
| 42 | + "output": "/usr/local/bin/golo\n", | |
| 43 | + "ref": "LlxPQH40AEgDIYIMc4s4WxRfhPJMEYcA" | |
| 44 | + } | |
| 45 | + } | |
| 46 | + ], | |
| 47 | + "role": "tool" | |
| 48 | + }, | |
| 49 | + { | |
| 50 | + "content": [ | |
| 51 | + { | |
| 52 | + "toolRequest": { | |
| 53 | + "input": { | |
| 54 | + "command": "golo --version" | |
| 55 | + }, | |
| 56 | + "name": "bash", | |
| 57 | + "ref": "11Ll8TJAVwPKMxEHuiKS0dk4qEvjGEt3" | |
| 58 | + } | |
| 59 | + } | |
| 60 | + ], | |
| 61 | + "role": "model" | |
| 62 | + }, | |
| 63 | + { | |
| 64 | + "content": [ | |
| 65 | + { | |
| 66 | + "toolResponse": { | |
| 67 | + "name": "bash", | |
| 68 | + "output": "v0.2.0 | dev.20260914.🤓\n", | |
| 69 | + "ref": "11Ll8TJAVwPKMxEHuiKS0dk4qEvjGEt3" | |
| 70 | + } | |
| 71 | + } | |
| 72 | + ], | |
| 73 | + "role": "tool" | |
| 74 | + }, | |
| 75 | + { | |
| 76 | + "content": [ | |
| 77 | + { | |
| 78 | + "text": "Yes, I know Golo! It's a dynamic language for the JVM that focuses on simplicity and expressiveness. The version you're seeing (v0.2.0) is quite recent, with a development version from September 2026. Golo aims to provide a lightweight alternative to languages like Groovy or Scala while maintaining good interoperability with Java.\n\nIs there something specific about Golo you'd like to know or discuss?" | |
| 79 | + } | |
| 80 | + ], | |
| 81 | + "role": "model" | |
| 82 | + }, | |
| 83 | + { | |
| 84 | + "content": [ | |
| 85 | + { | |
| 86 | + "text": "try ./golo-search struct create" | |
| 87 | + } | |
| 88 | + ], | |
| 89 | + "role": "user" | |
| 90 | + }, | |
| 91 | + { | |
| 92 | + "content": [ | |
| 93 | + { | |
| 94 | + "toolRequest": { | |
| 95 | + "input": { | |
| 96 | + "command": "./golo-search struct create --help" | |
| 97 | + }, | |
| 98 | + "name": "bash", | |
| 99 | + "ref": "Tv4nsGxb2Z8f4JKrqm6MDsxgl3CGVhyI" | |
| 100 | + } | |
| 101 | + } | |
| 102 | + ], | |
| 103 | + "role": "model" | |
| 104 | + }, | |
| 105 | + { | |
| 106 | + "content": [ | |
| 107 | + { | |
| 108 | + "toolResponse": { | |
| 109 | + "name": "bash", | |
| 110 | + "output": "bash: ./golo-search: No such file or directory\n\n[exit code: exit status 127]", | |
| 111 | + "ref": "Tv4nsGxb2Z8f4JKrqm6MDsxgl3CGVhyI" | |
| 112 | + } | |
| 113 | + } | |
| 114 | + ], | |
| 115 | + "role": "tool" | |
| 116 | + }, | |
| 117 | + { | |
| 118 | + "content": [ | |
| 119 | + { | |
| 120 | + "toolRequest": { | |
| 121 | + "input": { | |
| 122 | + "command": "find . -name \"golo-search\" -type f" | |
| 123 | + }, | |
| 124 | + "name": "bash", | |
| 125 | + "ref": "r5FmYBA7Op73zOOhVNfkTvGKH1QpCSKo" | |
| 126 | + } | |
| 127 | + } | |
| 128 | + ], | |
| 129 | + "role": "model" | |
| 130 | + }, | |
| 131 | + { | |
| 132 | + "content": [ | |
| 133 | + { | |
| 134 | + "toolResponse": { | |
| 135 | + "name": "bash", | |
| 136 | + "output": "", | |
| 137 | + "ref": "r5FmYBA7Op73zOOhVNfkTvGKH1QpCSKo" | |
| 138 | + } | |
| 139 | + } | |
| 140 | + ], | |
| 141 | + "role": "tool" | |
| 142 | + }, | |
| 143 | + { | |
| 144 | + "content": [ | |
| 145 | + { | |
| 146 | + "toolRequest": { | |
| 147 | + "input": { | |
| 148 | + "command": "ls -la" | |
| 149 | + }, | |
| 150 | + "name": "bash", | |
| 151 | + "ref": "5N5m7SF5ghnfxAiVpgMbEf7OrYWcfh8L" | |
| 152 | + } | |
| 153 | + } | |
| 154 | + ], | |
| 155 | + "role": "model" | |
| 156 | + }, | |
| 157 | + { | |
| 158 | + "content": [ | |
| 159 | + { | |
| 160 | + "toolResponse": { | |
| 161 | + "name": "bash", | |
| 162 | + "output": "total 8\ndrwxr-xr-x@ 5 k33g staff 160 Sep 16 07:25 .\ndrwxr-xr-x@ 10 k33g staff 320 Sep 16 07:20 ..\ndrwxr-xr-x@ 3 k33g staff 96 Sep 16 07:25 .mm\ndrwxr-xr-x@ 7 k33g staff 224 Sep 16 07:24 .turbo-golo\n-rw-r--r--@ 1 k33g staff 70 Sep 16 07:24 hello.golo\n", | |
| 163 | + "ref": "5N5m7SF5ghnfxAiVpgMbEf7OrYWcfh8L" | |
| 164 | + } | |
| 165 | + } | |
| 166 | + ], | |
| 167 | + "role": "tool" | |
| 168 | + }, | |
| 169 | + { | |
| 170 | + "content": [ | |
| 171 | + { | |
| 172 | + "toolRequest": { | |
| 173 | + "input": { | |
| 174 | + "command": "ls -la .mm/" | |
| 175 | + }, | |
| 176 | + "name": "bash", | |
| 177 | + "ref": "2iSYocyK67JB6VAfqdlgJZY0SP9UdBuF" | |
| 178 | + } | |
| 179 | + } | |
| 180 | + ], | |
| 181 | + "role": "model" | |
| 182 | + }, | |
| 183 | + { | |
| 184 | + "content": [ | |
| 185 | + { | |
| 186 | + "toolResponse": { | |
| 187 | + "name": "bash", | |
| 188 | + "output": "total 0\ndrwxr-xr-x@ 3 k33g staff 96 Sep 16 07:25 .\ndrwxr-xr-x@ 5 k33g staff 160 Sep 16 07:25 ..\ndrwxr-xr-x@ 3 k33g staff 96 Sep 16 07:26 sessions\n", | |
| 189 | + "ref": "2iSYocyK67JB6VAfqdlgJZY0SP9UdBuF" | |
| 190 | + } | |
| 191 | + } | |
| 192 | + ], | |
| 193 | + "role": "tool" | |
| 194 | + }, | |
| 195 | + { | |
| 196 | + "content": [ | |
| 197 | + { | |
| 198 | + "toolRequest": { | |
| 199 | + "input": { | |
| 200 | + "command": "ls -la .mm/sessions/" | |
| 201 | + }, | |
| 202 | + "name": "bash", | |
| 203 | + "ref": "hF1pZ3MrX4tFNLTghAIyidTVnVFiX4vG" | |
| 204 | + } | |
| 205 | + } | |
| 206 | + ], | |
| 207 | + "role": "model" | |
| 208 | + }, | |
| 209 | + { | |
| 210 | + "content": [ | |
| 211 | + { | |
| 212 | + "toolResponse": { | |
| 213 | + "name": "bash", | |
| 214 | + "output": "total 16\ndrwxr-xr-x@ 3 k33g staff 96 Sep 16 07:26 .\ndrwxr-xr-x@ 3 k33g staff 96 Sep 16 07:25 ..\n-rw-r--r--@ 1 k33g staff 5638 Sep 16 07:26 20260916-052534-64c5f107.json\n", | |
| 215 | + "ref": "hF1pZ3MrX4tFNLTghAIyidTVnVFiX4vG" | |
| 216 | + } | |
| 217 | + } | |
| 218 | + ], | |
| 219 | + "role": "tool" | |
| 220 | + }, | |
| 221 | + { | |
| 222 | + "content": [ | |
| 223 | + { | |
| 224 | + "toolRequest": { | |
| 225 | + "input": { | |
| 226 | + "command": "cat .mm/sessions/20260916-052534-64c5f107.json | head -n 20" | |
| 227 | + }, | |
| 228 | + "name": "bash", | |
| 229 | + "ref": "wvDfBqFuaQzmmpwGPBtEJrHhSfQFaAni" | |
| 230 | + } | |
| 231 | + } | |
| 232 | + ], | |
| 233 | + "role": "model" | |
| 234 | + }, | |
| 235 | + { | |
| 236 | + "content": [ | |
| 237 | + { | |
| 238 | + "toolResponse": { | |
| 239 | + "name": "bash", | |
| 240 | + "output": "{\n \"id\": \"20260916-052534-64c5f107\",\n \"cwd\": \"/Users/k33g/CodeBerg/turbo-editors/turbo-golo/demos/hello-golo\",\n \"createdAt\": \"2026-09-16T05:25:34.343495Z\",\n \"updatedAt\": \"2026-09-16T05:26:01.113518Z\",\n \"messages\": [\n {\n \"content\": [\n {\n \"text\": \"Your name is Bob.\\nYou are a coding agent working in a terminal.\\nYou have a \\\"bash\\\" tool to run shell commands.\\nUse it to explore files, run tests, inspect the repository, etc.\\nChain several commands if needed, then answer clearly in English.\\n\\nA request often mixes things you answer from yourself (\\\"say hello\\\") with\\nthings only a command can answer (\\\"list the files\\\"). Handle every part, in\\nthe order asked, and run a command for each part that needs one.\\nNever state the contents of a file, the output of a command, or the state of\\nthe repository unless a command in THIS answer returned it. What you did not\\nread, you do not know: run the command instead of recalling it.\\n\\nSKILLS\\nYou have a second tool, `read_skill`. Its description lists the procedures\\navailable for this project — one per kind of task.\\n\\nAny request to DO something to a Go project is a skill, not a shell command\\nyou invent. Match the request against that list, call `read_skill` FIRST,\\nbefore any bash command, and then follow what it says step by step.\\n\\nFILE EDITING\\nYou have three tools for files: `read_file`, `edit_file` and `write_file`.\\nThey are how a file gets read and changed here: each change is exact,\\nchecked before it is written, and comes back as a diff with line numbers.\\nbash is for running things — building, testing, listing, searching.\\n\\n- Read before you write: call `read_file` on the file (numbered=true when\\n you need line numbers). You cannot target text you have not seen; never\\n rely on what you think you remember about a file.\\n- To change an existing file, call `edit_file` with one or more {old, new}\\n pairs. `old` is copied from the file character for character — same\\n spaces, same indentation, same line breaks — and appears exactly once:\\n add the surrounding lines until it is unique. Several pairs are applied\\n together, against the original file. An empty `new` deletes the text.\\n- Call `write_file` only to create a file, or to rewrite one entirely and\\n on purpose. On an existing file it replaces everything, including what\\n you did not intend to touch.\\n- Read the diff the tool returns: it says exactly what changed and on which\\n line. If `edit_file` refuses — text not found, ambiguous, overlapping\\n edits — read the file again and fix `old`. Do not fall back to\\n `write_file` to force the change through.\\n- After editing code, run the narrowest check with bash: the formatter, the\\n compiler, or the test covering that file.\\n\\nRULES\\n- Keep everything the file already does, unless the user asked to remove it.\\n- Touch only the files the request is about. Do not add tests, files or\\n features that were not asked for.\\n- Never run a git command unless the user says git, commit or push.\\n- Never move, rename or delete a file unless the user asked for it.\\n- Then answer in English, in a few lines.\\n- If you don't know how to use a \\u003ccli\\u003e, run `\\u003ccli\\u003e --help` (or `\\u003ccli\\u003e help`)\\n to understand the options, then run the command.\\n\\nBACKGROUND JOBS\\nNever let a command block the answer. Anything that serves, watches or runs\\nlong goes to the background, with BOTH streams redirected and its pid kept:\\n\\n nohup \\u003ccommand\\u003e \\u003e /tmp/\\u003cjob\\u003e.log 2\\u003e\\u00261 \\u0026 echo $! \\u003e /tmp/\\u003cjob\\u003e.pid\\n\\nRedirecting only stdout still blocks until the process exits. Read the\\n`bg-jobs` skill before you wait on, inspect or stop such a job — each has a\\nrule you cannot guess. Stop every job you started before you finish, and say\\nwhich ones you left running.\\n\"\n }\n ],\n \"role\": \"system\"\n },\n {\n \"content\": [\n {\n \"text\": \"do you know golo?\"\n }\n ],\n", | |
| 241 | + "ref": "wvDfBqFuaQzmmpwGPBtEJrHhSfQFaAni" | |
| 242 | + } | |
| 243 | + } | |
| 244 | + ], | |
| 245 | + "role": "tool" | |
| 246 | + }, | |
| 247 | + { | |
| 248 | + "content": [ | |
| 249 | + { | |
| 250 | + "text": "It seems there's no `golo-search` command in this environment. The available tools are focused on file operations and the `golo` language itself. If you'd like to explore Golo's struct functionality, I can help with that directly." | |
| 251 | + } | |
| 252 | + ], | |
| 253 | + "role": "model" | |
| 254 | + } | |
| 255 | + ] | |
| 256 | +} | |
| \ No newline at end of file | ||
| new file mode 100644 | |||
| @@ -0,0 +1,256 @@ | |||
| 1 | +{ | ||
| 2 | + "id": "20260916-052534-64c5f107", | ||
| 3 | + "cwd": "/Users/k33g/CodeBerg/turbo-editors/turbo-golo/demos/hello-golo", | ||
| 4 | + "createdAt": "2026-09-16T05:25:34.343495Z", | ||
| 5 | + "updatedAt": "2026-09-16T05:26:36.412951Z", | ||
| 6 | + "messages": [ | ||
| 7 | + { | ||
| 8 | + "content": [ | ||
| 9 | + { | ||
| 10 | + "text": "Your name is Bob.\nYou are a coding agent working in a terminal.\nYou have a \"bash\" tool to run shell commands.\nUse it to explore files, run tests, inspect the repository, etc.\nChain several commands if needed, then answer clearly in English.\n\nA request often mixes things you answer from yourself (\"say hello\") with\nthings only a command can answer (\"list the files\"). Handle every part, in\nthe order asked, and run a command for each part that needs one.\nNever state the contents of a file, the output of a command, or the state of\nthe repository unless a command in THIS answer returned it. What you did not\nread, you do not know: run the command instead of recalling it.\n\nSKILLS\nYou have a second tool, `read_skill`. Its description lists the procedures\navailable for this project — one per kind of task.\n\nAny request to DO something to a Go project is a skill, not a shell command\nyou invent. Match the request against that list, call `read_skill` FIRST,\nbefore any bash command, and then follow what it says step by step.\n\nFILE EDITING\nYou have three tools for files: `read_file`, `edit_file` and `write_file`.\nThey are how a file gets read and changed here: each change is exact,\nchecked before it is written, and comes back as a diff with line numbers.\nbash is for running things — building, testing, listing, searching.\n\n- Read before you write: call `read_file` on the file (numbered=true when\n you need line numbers). You cannot target text you have not seen; never\n rely on what you think you remember about a file.\n- To change an existing file, call `edit_file` with one or more {old, new}\n pairs. `old` is copied from the file character for character — same\n spaces, same indentation, same line breaks — and appears exactly once:\n add the surrounding lines until it is unique. Several pairs are applied\n together, against the original file. An empty `new` deletes the text.\n- Call `write_file` only to create a file, or to rewrite one entirely and\n on purpose. On an existing file it replaces everything, including what\n you did not intend to touch.\n- Read the diff the tool returns: it says exactly what changed and on which\n line. If `edit_file` refuses — text not found, ambiguous, overlapping\n edits — read the file again and fix `old`. Do not fall back to\n `write_file` to force the change through.\n- After editing code, run the narrowest check with bash: the formatter, the\n compiler, or the test covering that file.\n\nRULES\n- Keep everything the file already does, unless the user asked to remove it.\n- Touch only the files the request is about. Do not add tests, files or\n features that were not asked for.\n- Never run a git command unless the user says git, commit or push.\n- Never move, rename or delete a file unless the user asked for it.\n- Then answer in English, in a few lines.\n- If you don't know how to use a \u003ccli\u003e, run `\u003ccli\u003e --help` (or `\u003ccli\u003e help`)\n to understand the options, then run the command.\n\nBACKGROUND JOBS\nNever let a command block the answer. Anything that serves, watches or runs\nlong goes to the background, with BOTH streams redirected and its pid kept:\n\n nohup \u003ccommand\u003e \u003e /tmp/\u003cjob\u003e.log 2\u003e\u00261 \u0026 echo $! \u003e /tmp/\u003cjob\u003e.pid\n\nRedirecting only stdout still blocks until the process exits. Read the\n`bg-jobs` skill before you wait on, inspect or stop such a job — each has a\nrule you cannot guess. Stop every job you started before you finish, and say\nwhich ones you left running.\n" | ||
| 11 | + } | ||
| 12 | + ], | ||
| 13 | + "role": "system" | ||
| 14 | + }, | ||
| 15 | + { | ||
| 16 | + "content": [ | ||
| 17 | + { | ||
| 18 | + "text": "do you know golo?" | ||
| 19 | + } | ||
| 20 | + ], | ||
| 21 | + "role": "user" | ||
| 22 | + }, | ||
| 23 | + { | ||
| 24 | + "content": [ | ||
| 25 | + { | ||
| 26 | + "toolRequest": { | ||
| 27 | + "input": { | ||
| 28 | + "command": "which golo" | ||
| 29 | + }, | ||
| 30 | + "name": "bash", | ||
| 31 | + "ref": "LlxPQH40AEgDIYIMc4s4WxRfhPJMEYcA" | ||
| 32 | + } | ||
| 33 | + } | ||
| 34 | + ], | ||
| 35 | + "role": "model" | ||
| 36 | + }, | ||
| 37 | + { | ||
| 38 | + "content": [ | ||
| 39 | + { | ||
| 40 | + "toolResponse": { | ||
| 41 | + "name": "bash", | ||
| 42 | + "output": "/usr/local/bin/golo\n", | ||
| 43 | + "ref": "LlxPQH40AEgDIYIMc4s4WxRfhPJMEYcA" | ||
| 44 | + } | ||
| 45 | + } | ||
| 46 | + ], | ||
| 47 | + "role": "tool" | ||
| 48 | + }, | ||
| 49 | + { | ||
| 50 | + "content": [ | ||
| 51 | + { | ||
| 52 | + "toolRequest": { | ||
| 53 | + "input": { | ||
| 54 | + "command": "golo --version" | ||
| 55 | + }, | ||
| 56 | + "name": "bash", | ||
| 57 | + "ref": "11Ll8TJAVwPKMxEHuiKS0dk4qEvjGEt3" | ||
| 58 | + } | ||
| 59 | + } | ||
| 60 | + ], | ||
| 61 | + "role": "model" | ||
| 62 | + }, | ||
| 63 | + { | ||
| 64 | + "content": [ | ||
| 65 | + { | ||
| 66 | + "toolResponse": { | ||
| 67 | + "name": "bash", | ||
| 68 | + "output": "v0.2.0 | dev.20260914.🤓\n", | ||
| 69 | + "ref": "11Ll8TJAVwPKMxEHuiKS0dk4qEvjGEt3" | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | + ], | ||
| 73 | + "role": "tool" | ||
| 74 | + }, | ||
| 75 | + { | ||
| 76 | + "content": [ | ||
| 77 | + { | ||
| 78 | + "text": "Yes, I know Golo! It's a dynamic language for the JVM that focuses on simplicity and expressiveness. The version you're seeing (v0.2.0) is quite recent, with a development version from September 2026. Golo aims to provide a lightweight alternative to languages like Groovy or Scala while maintaining good interoperability with Java.\n\nIs there something specific about Golo you'd like to know or discuss?" | ||
| 79 | + } | ||
| 80 | + ], | ||
| 81 | + "role": "model" | ||
| 82 | + }, | ||
| 83 | + { | ||
| 84 | + "content": [ | ||
| 85 | + { | ||
| 86 | + "text": "try ./golo-search struct create" | ||
| 87 | + } | ||
| 88 | + ], | ||
| 89 | + "role": "user" | ||
| 90 | + }, | ||
| 91 | + { | ||
| 92 | + "content": [ | ||
| 93 | + { | ||
| 94 | + "toolRequest": { | ||
| 95 | + "input": { | ||
| 96 | + "command": "./golo-search struct create --help" | ||
| 97 | + }, | ||
| 98 | + "name": "bash", | ||
| 99 | + "ref": "Tv4nsGxb2Z8f4JKrqm6MDsxgl3CGVhyI" | ||
| 100 | + } | ||
| 101 | + } | ||
| 102 | + ], | ||
| 103 | + "role": "model" | ||
| 104 | + }, | ||
| 105 | + { | ||
| 106 | + "content": [ | ||
| 107 | + { | ||
| 108 | + "toolResponse": { | ||
| 109 | + "name": "bash", | ||
| 110 | + "output": "bash: ./golo-search: No such file or directory\n\n[exit code: exit status 127]", | ||
| 111 | + "ref": "Tv4nsGxb2Z8f4JKrqm6MDsxgl3CGVhyI" | ||
| 112 | + } | ||
| 113 | + } | ||
| 114 | + ], | ||
| 115 | + "role": "tool" | ||
| 116 | + }, | ||
| 117 | + { | ||
| 118 | + "content": [ | ||
| 119 | + { | ||
| 120 | + "toolRequest": { | ||
| 121 | + "input": { | ||
| 122 | + "command": "find . -name \"golo-search\" -type f" | ||
| 123 | + }, | ||
| 124 | + "name": "bash", | ||
| 125 | + "ref": "r5FmYBA7Op73zOOhVNfkTvGKH1QpCSKo" | ||
| 126 | + } | ||
| 127 | + } | ||
| 128 | + ], | ||
| 129 | + "role": "model" | ||
| 130 | + }, | ||
| 131 | + { | ||
| 132 | + "content": [ | ||
| 133 | + { | ||
| 134 | + "toolResponse": { | ||
| 135 | + "name": "bash", | ||
| 136 | + "output": "", | ||
| 137 | + "ref": "r5FmYBA7Op73zOOhVNfkTvGKH1QpCSKo" | ||
| 138 | + } | ||
| 139 | + } | ||
| 140 | + ], | ||
| 141 | + "role": "tool" | ||
| 142 | + }, | ||
| 143 | + { | ||
| 144 | + "content": [ | ||
| 145 | + { | ||
| 146 | + "toolRequest": { | ||
| 147 | + "input": { | ||
| 148 | + "command": "ls -la" | ||
| 149 | + }, | ||
| 150 | + "name": "bash", | ||
| 151 | + "ref": "5N5m7SF5ghnfxAiVpgMbEf7OrYWcfh8L" | ||
| 152 | + } | ||
| 153 | + } | ||
| 154 | + ], | ||
| 155 | + "role": "model" | ||
| 156 | + }, | ||
| 157 | + { | ||
| 158 | + "content": [ | ||
| 159 | + { | ||
| 160 | + "toolResponse": { | ||
| 161 | + "name": "bash", | ||
| 162 | + "output": "total 8\ndrwxr-xr-x@ 5 k33g staff 160 Sep 16 07:25 .\ndrwxr-xr-x@ 10 k33g staff 320 Sep 16 07:20 ..\ndrwxr-xr-x@ 3 k33g staff 96 Sep 16 07:25 .mm\ndrwxr-xr-x@ 7 k33g staff 224 Sep 16 07:24 .turbo-golo\n-rw-r--r--@ 1 k33g staff 70 Sep 16 07:24 hello.golo\n", | ||
| 163 | + "ref": "5N5m7SF5ghnfxAiVpgMbEf7OrYWcfh8L" | ||
| 164 | + } | ||
| 165 | + } | ||
| 166 | + ], | ||
| 167 | + "role": "tool" | ||
| 168 | + }, | ||
| 169 | + { | ||
| 170 | + "content": [ | ||
| 171 | + { | ||
| 172 | + "toolRequest": { | ||
| 173 | + "input": { | ||
| 174 | + "command": "ls -la .mm/" | ||
| 175 | + }, | ||
| 176 | + "name": "bash", | ||
| 177 | + "ref": "2iSYocyK67JB6VAfqdlgJZY0SP9UdBuF" | ||
| 178 | + } | ||
| 179 | + } | ||
| 180 | + ], | ||
| 181 | + "role": "model" | ||
| 182 | + }, | ||
| 183 | + { | ||
| 184 | + "content": [ | ||
| 185 | + { | ||
| 186 | + "toolResponse": { | ||
| 187 | + "name": "bash", | ||
| 188 | + "output": "total 0\ndrwxr-xr-x@ 3 k33g staff 96 Sep 16 07:25 .\ndrwxr-xr-x@ 5 k33g staff 160 Sep 16 07:25 ..\ndrwxr-xr-x@ 3 k33g staff 96 Sep 16 07:26 sessions\n", | ||
| 189 | + "ref": "2iSYocyK67JB6VAfqdlgJZY0SP9UdBuF" | ||
| 190 | + } | ||
| 191 | + } | ||
| 192 | + ], | ||
| 193 | + "role": "tool" | ||
| 194 | + }, | ||
| 195 | + { | ||
| 196 | + "content": [ | ||
| 197 | + { | ||
| 198 | + "toolRequest": { | ||
| 199 | + "input": { | ||
| 200 | + "command": "ls -la .mm/sessions/" | ||
| 201 | + }, | ||
| 202 | + "name": "bash", | ||
| 203 | + "ref": "hF1pZ3MrX4tFNLTghAIyidTVnVFiX4vG" | ||
| 204 | + } | ||
| 205 | + } | ||
| 206 | + ], | ||
| 207 | + "role": "model" | ||
| 208 | + }, | ||
| 209 | + { | ||
| 210 | + "content": [ | ||
| 211 | + { | ||
| 212 | + "toolResponse": { | ||
| 213 | + "name": "bash", | ||
| 214 | + "output": "total 16\ndrwxr-xr-x@ 3 k33g staff 96 Sep 16 07:26 .\ndrwxr-xr-x@ 3 k33g staff 96 Sep 16 07:25 ..\n-rw-r--r--@ 1 k33g staff 5638 Sep 16 07:26 20260916-052534-64c5f107.json\n", | ||
| 215 | + "ref": "hF1pZ3MrX4tFNLTghAIyidTVnVFiX4vG" | ||
| 216 | + } | ||
| 217 | + } | ||
| 218 | + ], | ||
| 219 | + "role": "tool" | ||
| 220 | + }, | ||
| 221 | + { | ||
| 222 | + "content": [ | ||
| 223 | + { | ||
| 224 | + "toolRequest": { | ||
| 225 | + "input": { | ||
| 226 | + "command": "cat .mm/sessions/20260916-052534-64c5f107.json | head -n 20" | ||
| 227 | + }, | ||
| 228 | + "name": "bash", | ||
| 229 | + "ref": "wvDfBqFuaQzmmpwGPBtEJrHhSfQFaAni" | ||
| 230 | + } | ||
| 231 | + } | ||
| 232 | + ], | ||
| 233 | + "role": "model" | ||
| 234 | + }, | ||
| 235 | + { | ||
| 236 | + "content": [ | ||
| 237 | + { | ||
| 238 | + "toolResponse": { | ||
| 239 | + "name": "bash", | ||
| 240 | + "output": "{\n \"id\": \"20260916-052534-64c5f107\",\n \"cwd\": \"/Users/k33g/CodeBerg/turbo-editors/turbo-golo/demos/hello-golo\",\n \"createdAt\": \"2026-09-16T05:25:34.343495Z\",\n \"updatedAt\": \"2026-09-16T05:26:01.113518Z\",\n \"messages\": [\n {\n \"content\": [\n {\n \"text\": \"Your name is Bob.\\nYou are a coding agent working in a terminal.\\nYou have a \\\"bash\\\" tool to run shell commands.\\nUse it to explore files, run tests, inspect the repository, etc.\\nChain several commands if needed, then answer clearly in English.\\n\\nA request often mixes things you answer from yourself (\\\"say hello\\\") with\\nthings only a command can answer (\\\"list the files\\\"). Handle every part, in\\nthe order asked, and run a command for each part that needs one.\\nNever state the contents of a file, the output of a command, or the state of\\nthe repository unless a command in THIS answer returned it. What you did not\\nread, you do not know: run the command instead of recalling it.\\n\\nSKILLS\\nYou have a second tool, `read_skill`. Its description lists the procedures\\navailable for this project — one per kind of task.\\n\\nAny request to DO something to a Go project is a skill, not a shell command\\nyou invent. Match the request against that list, call `read_skill` FIRST,\\nbefore any bash command, and then follow what it says step by step.\\n\\nFILE EDITING\\nYou have three tools for files: `read_file`, `edit_file` and `write_file`.\\nThey are how a file gets read and changed here: each change is exact,\\nchecked before it is written, and comes back as a diff with line numbers.\\nbash is for running things — building, testing, listing, searching.\\n\\n- Read before you write: call `read_file` on the file (numbered=true when\\n you need line numbers). You cannot target text you have not seen; never\\n rely on what you think you remember about a file.\\n- To change an existing file, call `edit_file` with one or more {old, new}\\n pairs. `old` is copied from the file character for character — same\\n spaces, same indentation, same line breaks — and appears exactly once:\\n add the surrounding lines until it is unique. Several pairs are applied\\n together, against the original file. An empty `new` deletes the text.\\n- Call `write_file` only to create a file, or to rewrite one entirely and\\n on purpose. On an existing file it replaces everything, including what\\n you did not intend to touch.\\n- Read the diff the tool returns: it says exactly what changed and on which\\n line. If `edit_file` refuses — text not found, ambiguous, overlapping\\n edits — read the file again and fix `old`. Do not fall back to\\n `write_file` to force the change through.\\n- After editing code, run the narrowest check with bash: the formatter, the\\n compiler, or the test covering that file.\\n\\nRULES\\n- Keep everything the file already does, unless the user asked to remove it.\\n- Touch only the files the request is about. Do not add tests, files or\\n features that were not asked for.\\n- Never run a git command unless the user says git, commit or push.\\n- Never move, rename or delete a file unless the user asked for it.\\n- Then answer in English, in a few lines.\\n- If you don't know how to use a \\u003ccli\\u003e, run `\\u003ccli\\u003e --help` (or `\\u003ccli\\u003e help`)\\n to understand the options, then run the command.\\n\\nBACKGROUND JOBS\\nNever let a command block the answer. Anything that serves, watches or runs\\nlong goes to the background, with BOTH streams redirected and its pid kept:\\n\\n nohup \\u003ccommand\\u003e \\u003e /tmp/\\u003cjob\\u003e.log 2\\u003e\\u00261 \\u0026 echo $! \\u003e /tmp/\\u003cjob\\u003e.pid\\n\\nRedirecting only stdout still blocks until the process exits. Read the\\n`bg-jobs` skill before you wait on, inspect or stop such a job — each has a\\nrule you cannot guess. Stop every job you started before you finish, and say\\nwhich ones you left running.\\n\"\n }\n ],\n \"role\": \"system\"\n },\n {\n \"content\": [\n {\n \"text\": \"do you know golo?\"\n }\n ],\n", | ||
| 241 | + "ref": "wvDfBqFuaQzmmpwGPBtEJrHhSfQFaAni" | ||
| 242 | + } | ||
| 243 | + } | ||
| 244 | + ], | ||
| 245 | + "role": "tool" | ||
| 246 | + }, | ||
| 247 | + { | ||
| 248 | + "content": [ | ||
| 249 | + { | ||
| 250 | + "text": "It seems there's no `golo-search` command in this environment. The available tools are focused on file operations and the `golo` language itself. If you'd like to explore Golo's struct functionality, I can help with that directly." | ||
| 251 | + } | ||
| 252 | + ], | ||
| 253 | + "role": "model" | ||
| 254 | + } | ||
| 255 | + ] | ||
| 256 | +} | ||
| \ No newline at end of file | \ No newline at end of file | ||
added
demos/hello-golo/.mm/sessions/20260916-052931-d9c1d120.json +126 -0 | new file mode 100644 | ||
| @@ -0,0 +1,126 @@ | ||
| 1 | +{ | |
| 2 | + "id": "20260916-052931-d9c1d120", | |
| 3 | + "cwd": "/Users/k33g/CodeBerg/turbo-editors/turbo-golo/demos/hello-golo", | |
| 4 | + "createdAt": "2026-09-16T05:29:31.460981Z", | |
| 5 | + "updatedAt": "2026-09-16T05:30:59.993928Z", | |
| 6 | + "messages": [ | |
| 7 | + { | |
| 8 | + "content": [ | |
| 9 | + { | |
| 10 | + "text": "Your name is Bob.\nYou are a coding agent working in a terminal.\nYou have a \"bash\" tool to run shell commands.\nUse it to explore files, run tests, inspect the repository, etc.\nChain several commands if needed, then answer clearly in English.\n\nA request often mixes things you answer from yourself (\"say hello\") with\nthings only a command can answer (\"list the files\"). Handle every part, in\nthe order asked, and run a command for each part that needs one.\nNever state the contents of a file, the output of a command, or the state of\nthe repository unless a command in THIS answer returned it. What you did not\nread, you do not know: run the command instead of recalling it.\n\nSKILLS\nYou have a second tool, `read_skill`. Its description lists the procedures\navailable for this project — one per kind of task.\n\nAny request to DO something to a Go project is a skill, not a shell command\nyou invent. Match the request against that list, call `read_skill` FIRST,\nbefore any bash command, and then follow what it says step by step.\n\nFILE EDITING\nYou have three tools for files: `read_file`, `edit_file` and `write_file`.\nThey are how a file gets read and changed here: each change is exact,\nchecked before it is written, and comes back as a diff with line numbers.\nbash is for running things — building, testing, listing, searching.\n\n- Read before you write: call `read_file` on the file (numbered=true when\n you need line numbers). You cannot target text you have not seen; never\n rely on what you think you remember about a file.\n- To change an existing file, call `edit_file` with one or more {old, new}\n pairs. `old` is copied from the file character for character — same\n spaces, same indentation, same line breaks — and appears exactly once:\n add the surrounding lines until it is unique. Several pairs are applied\n together, against the original file. An empty `new` deletes the text.\n- Call `write_file` only to create a file, or to rewrite one entirely and\n on purpose. On an existing file it replaces everything, including what\n you did not intend to touch.\n- Read the diff the tool returns: it says exactly what changed and on which\n line. If `edit_file` refuses — text not found, ambiguous, overlapping\n edits — read the file again and fix `old`. Do not fall back to\n `write_file` to force the change through.\n- After editing code, run the narrowest check with bash: the formatter, the\n compiler, or the test covering that file.\n\nRULES\n- Keep everything the file already does, unless the user asked to remove it.\n- Touch only the files the request is about. Do not add tests, files or\n features that were not asked for.\n- Never run a git command unless the user says git, commit or push.\n- Never move, rename or delete a file unless the user asked for it.\n- Then answer in English, in a few lines.\n- If you don't know how to use a \u003ccli\u003e, run `\u003ccli\u003e --help` (or `\u003ccli\u003e help`)\n to understand the options, then run the command.\n\nBACKGROUND JOBS\nNever let a command block the answer. Anything that serves, watches or runs\nlong goes to the background, with BOTH streams redirected and its pid kept:\n\n nohup \u003ccommand\u003e \u003e /tmp/\u003cjob\u003e.log 2\u003e\u00261 \u0026 echo $! \u003e /tmp/\u003cjob\u003e.pid\n\nRedirecting only stdout still blocks until the process exits. Read the\n`bg-jobs` skill before you wait on, inspect or stop such a job — each has a\nrule you cannot guess. Stop every job you started before you finish, and say\nwhich ones you left running.\n" | |
| 11 | + } | |
| 12 | + ], | |
| 13 | + "role": "system" | |
| 14 | + }, | |
| 15 | + { | |
| 16 | + "content": [ | |
| 17 | + { | |
| 18 | + "text": "do you know golo?" | |
| 19 | + } | |
| 20 | + ], | |
| 21 | + "role": "user" | |
| 22 | + }, | |
| 23 | + { | |
| 24 | + "content": [ | |
| 25 | + { | |
| 26 | + "toolRequest": { | |
| 27 | + "input": { | |
| 28 | + "command": "which golo" | |
| 29 | + }, | |
| 30 | + "name": "bash", | |
| 31 | + "ref": "qTAefGXBSeQuYoFBrArK6vgxXb3OmrlC" | |
| 32 | + } | |
| 33 | + } | |
| 34 | + ], | |
| 35 | + "role": "model" | |
| 36 | + }, | |
| 37 | + { | |
| 38 | + "content": [ | |
| 39 | + { | |
| 40 | + "toolResponse": { | |
| 41 | + "name": "bash", | |
| 42 | + "output": "/usr/local/bin/golo\n", | |
| 43 | + "ref": "qTAefGXBSeQuYoFBrArK6vgxXb3OmrlC" | |
| 44 | + } | |
| 45 | + } | |
| 46 | + ], | |
| 47 | + "role": "tool" | |
| 48 | + }, | |
| 49 | + { | |
| 50 | + "content": [ | |
| 51 | + { | |
| 52 | + "toolRequest": { | |
| 53 | + "input": { | |
| 54 | + "command": "golo --version" | |
| 55 | + }, | |
| 56 | + "name": "bash", | |
| 57 | + "ref": "m8Mh5UiZiobEXn4EhToV1K6cDstICBoC" | |
| 58 | + } | |
| 59 | + } | |
| 60 | + ], | |
| 61 | + "role": "model" | |
| 62 | + }, | |
| 63 | + { | |
| 64 | + "content": [ | |
| 65 | + { | |
| 66 | + "toolResponse": { | |
| 67 | + "name": "bash", | |
| 68 | + "output": "v0.2.0 | dev.20260914.🤓\n", | |
| 69 | + "ref": "m8Mh5UiZiobEXn4EhToV1K6cDstICBoC" | |
| 70 | + } | |
| 71 | + } | |
| 72 | + ], | |
| 73 | + "role": "tool" | |
| 74 | + }, | |
| 75 | + { | |
| 76 | + "content": [ | |
| 77 | + { | |
| 78 | + "text": "Yes, I know Golo! It's a lightweight, dynamic scripting language for the JVM, designed to be simple and easy to use. It's often used for automation, build scripts, and prototyping. The version you're seeing (v0.2.0) is a development release from September 2026. Let me know if you'd like to explore its features or use it for a specific task!" | |
| 79 | + } | |
| 80 | + ], | |
| 81 | + "role": "model" | |
| 82 | + }, | |
| 83 | + { | |
| 84 | + "content": [ | |
| 85 | + { | |
| 86 | + "text": "try this: ./tools/golo-search struct create, then explain what is a struct in golo" | |
| 87 | + } | |
| 88 | + ], | |
| 89 | + "role": "user" | |
| 90 | + }, | |
| 91 | + { | |
| 92 | + "content": [ | |
| 93 | + { | |
| 94 | + "toolRequest": { | |
| 95 | + "input": { | |
| 96 | + "command": "./tools/golo-search struct create" | |
| 97 | + }, | |
| 98 | + "name": "bash", | |
| 99 | + "ref": "LSHzFiHW6r8er4gXXq0PNEfD9juiAmEJ" | |
| 100 | + } | |
| 101 | + } | |
| 102 | + ], | |
| 103 | + "role": "model" | |
| 104 | + }, | |
| 105 | + { | |
| 106 | + "content": [ | |
| 107 | + { | |
| 108 | + "toolResponse": { | |
| 109 | + "name": "bash", | |
| 110 | + "output": "1. Defining and creating\n file: ebook/en/volume-1-fundamentals/chapters/07-structs.adoc:7\n in: [en] volume-1-fundamentals \u003e Structs and augmentations \u003e Defining and creating\n matched: struct, create (score 4.6)\n\n[source,golo]\n----\nstruct Point = { x, y }\nstruct Person = { name, age }\nstruct Rectangle = { width, height }\n\nlet p1 = Point(10, 20)\nlet alice = Person(\"Alice\", 30)\n----\n\nEvery field gives an accessor of the same name. Called *without* an argument it reads; called *with* one it writes and returns the instance — which allows chaining:\n\n[source,golo]\n----\nprintln(p1: x()) # 10\nprintln(p1) # Point(x=10, y=20)\n\np1: x(100)\nprintln(p1) # Point(x=100, y=20)\n\nlet rect = Rectangle(0, 0)\n : width(50)\n : height(30)\nprintln(rect) # Rectangle(width=50, height=30)\n----\n\nThat last pattern is the *builder pattern*, for free: a neutral instance, then the fields that matter.\n------------------------------------------------------------------------\n2. Create, fill, read\n file: ebook/en/volume-1-fundamentals/chapters/09-dynamic-objects.adoc:7\n in: [en] volume-1-fundamentals \u003e Dynamic objects \u003e Create, fill, read\n matched: struct, create (score 3.8)\n\n[source,golo]\n----\nlet person = DynamicObject()\n : name(\"Alice\")\n : age(30)\n : city(\"Paris\")\n\nprintln(person: name()) # Alice\nprintln(person: age()) # 30\n----\n\nThe mechanism fits in one sentence: *`: name(value)` writes and returns the object; `: name()` reads*. It is the same accessor syntax as on a `struct`, except that no prior definition is needed — the property is born from its first write, and returning `this` makes the spelling chainable.\n------------------------------------------------------------------------\n3. Create, read, write\n file: ebook/en/volume-2-standard-library/chapters/08-observables.adoc:10\n in: [en] volume-2-standard-library \u003e Observables \u003e Create, read, write\n matched: create (score 2.5)\n\n[source,golo]\n----\nlet counter = observable(0)\nlet message = observable(\"Hello\")\n\nprintln(observableGet(counter)) # 0\n\nobservableSet(counter, 42)\nprintln(observableGet(counter)) # 42\n----\n\nSo far this is a variable with heavier syntax. What changes everything is subscription.\n------------------------------------------------------------------------\n4. Augmenting a struct\n file: ebook/en/volume-1-fundamentals/chapters/07-structs.adoc:85\n in: [en] volume-1-fundamentals \u003e Structs and augmentations \u003e Augmenting a struct\n matched: struct (score 2.3)\n\n`augment` adds methods to an existing struct. The first parameter is called `this` by convention and is the instance.\n\n[source,golo]\n----\nstruct Person = { name, age }\n\naugment Person {\n function greet = |this| -\u003e \"Hello, my name is \" + this: name()\n\n function isAdult = |this| -\u003e this: age() \u003e= 18\n\n function birthday = |this| {\n this: age(this: age() + 1)\n return this\n }\n}\n\nlet alice = Person(\"Alice\", 30)\nprintln(alice: greet()) # Hello, my name is Alice\nprintln(alice: isAdult()) # true\n\nalice: birthday(): birthday()\nprintln(alice: age()) # 32\n----\n\nReturning `this` from a method that modifies the instance makes it chainable, exactly like the generated accessors.\n------------------------------------------------------------------------\n5. The methods every struct already has\n file: ebook/en/volume-1-fundamentals/chapters/07-structs.adoc:37\n in: [en] volume-1-fundamentals \u003e Structs and augmentations \u003e The methods every struct already has\n matched: struct (score 2.3)\n\nWithout writing anything, every struct knows how to copy, compare and describe itself.\n\n[cols=\"2,3\",options=\"header\"]\n|===\n| Method | What it does\n| `copy()` | Mutable copy\n| `frozenCopy()` | Immutable copy\n| `isFrozen()` | Whether the instance is frozen\n| `equals(other)` / `hashCode()` | Structural equality\n| `compareTo(other)` | Comparison, field by field\n| `members()` / `values()` | Field names and values\n| `get(name)` / `set(name, val)` | Dynamic access by field name\n|===\n\n[source,golo]\n----\nlet alice = Person(\"Alice\", 30)\n\nlet bob = alice: copy(): name(\"Bob\"): age(28)\nprintln(alice) # Person(name=Alice, age=30) — unchanged\nprintln(bob) # Person(name=Bob, age=28)\n\nlet frozen = alice: frozenCopy()\nprintln(frozen: isFrozen()) # true\n# frozen: age(31) # Exception: cannot modify frozen struct instance\n----\n\nEquality is structural, and comparison follows field order — so here the name first, the age second:\n\n[source,golo]\n----\nprintln(Person(\"Alice\", 30): equals(Person(\"Alice\", 30))) # true\nprintln(Person(\"Alice\", 30) \u003c Person(\"Bob\", 25)) # true\nprintln(Person(\"Alice\", 30): compareTo(Person(\"Bob\", 25))) # -1\n----\n\nIntrospection gives access to fields by name, which makes generic code possible:\n\n[source,golo]\n----\nprintln(alice: members()) # [name, age]\nprintln(alice: values()) # [Alice, 30]\nprintln(alice: get(\"age\")) # 30\nalice: set(\"age\", 31)\n----\n", | |
| 111 | + "ref": "LSHzFiHW6r8er4gXXq0PNEfD9juiAmEJ" | |
| 112 | + } | |
| 113 | + } | |
| 114 | + ], | |
| 115 | + "role": "tool" | |
| 116 | + }, | |
| 117 | + { | |
| 118 | + "content": [ | |
| 119 | + { | |
| 120 | + "text": "In Golo, a **struct** is a user-defined data type that groups related fields together, similar to a class or record in other languages. It allows you to create instances with named fields and provides built-in methods for accessing, modifying, and inspecting the data.\n\n### Key Features of Structs in Golo:\n1. **Definition**: \n You define a struct using `struct Name = { field1, field2, ... }`. \n Example: \n ```golo\n struct Point = { x, y }\n ```\n\n2. **Instance Creation**: \n Create an instance by calling the struct name with values: \n ```golo\n let p1 = Point(10, 20)\n ```\n\n3. **Field Access**: \n - Read a field using `: fieldName()`: \n ```golo\n println(p1: x()) # 10\n ```\n - Write a field using `: fieldName(value)` (returns the instance for chaining): \n ```golo\n p1: x(100)\n println(p1: x()) # 100\n ```\n\n4. **Builder Pattern**: \n You can initialize a struct and then modify specific fields: \n ```golo\n let rect = Rectangle(0, 0)\n : width(50)\n : height(30)\n println(rect) # Rectangle(width=50, height=30)\n ```\n\n5. **Methods**: \n You can add methods to a struct using `augment`: \n ```golo\n augment Point {\n function distanceFromOrigin = |this| -\\u003e\n Math.sqrt(this: x() ** 2 + this: y() ** 2)\n }\n println(p1: distanceFromOrigin()) # Distance from origin\n ```\n\n6. **Built-in Methods**: \n Every struct has utility methods like: \n - `copy()` / `frozenCopy()` for cloning \n - `equals(other)` for comparison \n - `members()` / `values()` for introspection \n\nStructs in Golo are lightweight, immutable by default (unless modified), and designed for simplicity and ease of use in scripting and prototyping." | |
| 121 | + } | |
| 122 | + ], | |
| 123 | + "role": "model" | |
| 124 | + } | |
| 125 | + ] | |
| 126 | +} | |
| \ No newline at end of file | ||
| new file mode 100644 | |||
| @@ -0,0 +1,126 @@ | |||
| 1 | +{ | ||
| 2 | + "id": "20260916-052931-d9c1d120", | ||
| 3 | + "cwd": "/Users/k33g/CodeBerg/turbo-editors/turbo-golo/demos/hello-golo", | ||
| 4 | + "createdAt": "2026-09-16T05:29:31.460981Z", | ||
| 5 | + "updatedAt": "2026-09-16T05:30:59.993928Z", | ||
| 6 | + "messages": [ | ||
| 7 | + { | ||
| 8 | + "content": [ | ||
| 9 | + { | ||
| 10 | + "text": "Your name is Bob.\nYou are a coding agent working in a terminal.\nYou have a \"bash\" tool to run shell commands.\nUse it to explore files, run tests, inspect the repository, etc.\nChain several commands if needed, then answer clearly in English.\n\nA request often mixes things you answer from yourself (\"say hello\") with\nthings only a command can answer (\"list the files\"). Handle every part, in\nthe order asked, and run a command for each part that needs one.\nNever state the contents of a file, the output of a command, or the state of\nthe repository unless a command in THIS answer returned it. What you did not\nread, you do not know: run the command instead of recalling it.\n\nSKILLS\nYou have a second tool, `read_skill`. Its description lists the procedures\navailable for this project — one per kind of task.\n\nAny request to DO something to a Go project is a skill, not a shell command\nyou invent. Match the request against that list, call `read_skill` FIRST,\nbefore any bash command, and then follow what it says step by step.\n\nFILE EDITING\nYou have three tools for files: `read_file`, `edit_file` and `write_file`.\nThey are how a file gets read and changed here: each change is exact,\nchecked before it is written, and comes back as a diff with line numbers.\nbash is for running things — building, testing, listing, searching.\n\n- Read before you write: call `read_file` on the file (numbered=true when\n you need line numbers). You cannot target text you have not seen; never\n rely on what you think you remember about a file.\n- To change an existing file, call `edit_file` with one or more {old, new}\n pairs. `old` is copied from the file character for character — same\n spaces, same indentation, same line breaks — and appears exactly once:\n add the surrounding lines until it is unique. Several pairs are applied\n together, against the original file. An empty `new` deletes the text.\n- Call `write_file` only to create a file, or to rewrite one entirely and\n on purpose. On an existing file it replaces everything, including what\n you did not intend to touch.\n- Read the diff the tool returns: it says exactly what changed and on which\n line. If `edit_file` refuses — text not found, ambiguous, overlapping\n edits — read the file again and fix `old`. Do not fall back to\n `write_file` to force the change through.\n- After editing code, run the narrowest check with bash: the formatter, the\n compiler, or the test covering that file.\n\nRULES\n- Keep everything the file already does, unless the user asked to remove it.\n- Touch only the files the request is about. Do not add tests, files or\n features that were not asked for.\n- Never run a git command unless the user says git, commit or push.\n- Never move, rename or delete a file unless the user asked for it.\n- Then answer in English, in a few lines.\n- If you don't know how to use a \u003ccli\u003e, run `\u003ccli\u003e --help` (or `\u003ccli\u003e help`)\n to understand the options, then run the command.\n\nBACKGROUND JOBS\nNever let a command block the answer. Anything that serves, watches or runs\nlong goes to the background, with BOTH streams redirected and its pid kept:\n\n nohup \u003ccommand\u003e \u003e /tmp/\u003cjob\u003e.log 2\u003e\u00261 \u0026 echo $! \u003e /tmp/\u003cjob\u003e.pid\n\nRedirecting only stdout still blocks until the process exits. Read the\n`bg-jobs` skill before you wait on, inspect or stop such a job — each has a\nrule you cannot guess. Stop every job you started before you finish, and say\nwhich ones you left running.\n" | ||
| 11 | + } | ||
| 12 | + ], | ||
| 13 | + "role": "system" | ||
| 14 | + }, | ||
| 15 | + { | ||
| 16 | + "content": [ | ||
| 17 | + { | ||
| 18 | + "text": "do you know golo?" | ||
| 19 | + } | ||
| 20 | + ], | ||
| 21 | + "role": "user" | ||
| 22 | + }, | ||
| 23 | + { | ||
| 24 | + "content": [ | ||
| 25 | + { | ||
| 26 | + "toolRequest": { | ||
| 27 | + "input": { | ||
| 28 | + "command": "which golo" | ||
| 29 | + }, | ||
| 30 | + "name": "bash", | ||
| 31 | + "ref": "qTAefGXBSeQuYoFBrArK6vgxXb3OmrlC" | ||
| 32 | + } | ||
| 33 | + } | ||
| 34 | + ], | ||
| 35 | + "role": "model" | ||
| 36 | + }, | ||
| 37 | + { | ||
| 38 | + "content": [ | ||
| 39 | + { | ||
| 40 | + "toolResponse": { | ||
| 41 | + "name": "bash", | ||
| 42 | + "output": "/usr/local/bin/golo\n", | ||
| 43 | + "ref": "qTAefGXBSeQuYoFBrArK6vgxXb3OmrlC" | ||
| 44 | + } | ||
| 45 | + } | ||
| 46 | + ], | ||
| 47 | + "role": "tool" | ||
| 48 | + }, | ||
| 49 | + { | ||
| 50 | + "content": [ | ||
| 51 | + { | ||
| 52 | + "toolRequest": { | ||
| 53 | + "input": { | ||
| 54 | + "command": "golo --version" | ||
| 55 | + }, | ||
| 56 | + "name": "bash", | ||
| 57 | + "ref": "m8Mh5UiZiobEXn4EhToV1K6cDstICBoC" | ||
| 58 | + } | ||
| 59 | + } | ||
| 60 | + ], | ||
| 61 | + "role": "model" | ||
| 62 | + }, | ||
| 63 | + { | ||
| 64 | + "content": [ | ||
| 65 | + { | ||
| 66 | + "toolResponse": { | ||
| 67 | + "name": "bash", | ||
| 68 | + "output": "v0.2.0 | dev.20260914.🤓\n", | ||
| 69 | + "ref": "m8Mh5UiZiobEXn4EhToV1K6cDstICBoC" | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | + ], | ||
| 73 | + "role": "tool" | ||
| 74 | + }, | ||
| 75 | + { | ||
| 76 | + "content": [ | ||
| 77 | + { | ||
| 78 | + "text": "Yes, I know Golo! It's a lightweight, dynamic scripting language for the JVM, designed to be simple and easy to use. It's often used for automation, build scripts, and prototyping. The version you're seeing (v0.2.0) is a development release from September 2026. Let me know if you'd like to explore its features or use it for a specific task!" | ||
| 79 | + } | ||
| 80 | + ], | ||
| 81 | + "role": "model" | ||
| 82 | + }, | ||
| 83 | + { | ||
| 84 | + "content": [ | ||
| 85 | + { | ||
| 86 | + "text": "try this: ./tools/golo-search struct create, then explain what is a struct in golo" | ||
| 87 | + } | ||
| 88 | + ], | ||
| 89 | + "role": "user" | ||
| 90 | + }, | ||
| 91 | + { | ||
| 92 | + "content": [ | ||
| 93 | + { | ||
| 94 | + "toolRequest": { | ||
| 95 | + "input": { | ||
| 96 | + "command": "./tools/golo-search struct create" | ||
| 97 | + }, | ||
| 98 | + "name": "bash", | ||
| 99 | + "ref": "LSHzFiHW6r8er4gXXq0PNEfD9juiAmEJ" | ||
| 100 | + } | ||
| 101 | + } | ||
| 102 | + ], | ||
| 103 | + "role": "model" | ||
| 104 | + }, | ||
| 105 | + { | ||
| 106 | + "content": [ | ||
| 107 | + { | ||
| 108 | + "toolResponse": { | ||
| 109 | + "name": "bash", | ||
| 110 | + "output": "1. Defining and creating\n file: ebook/en/volume-1-fundamentals/chapters/07-structs.adoc:7\n in: [en] volume-1-fundamentals \u003e Structs and augmentations \u003e Defining and creating\n matched: struct, create (score 4.6)\n\n[source,golo]\n----\nstruct Point = { x, y }\nstruct Person = { name, age }\nstruct Rectangle = { width, height }\n\nlet p1 = Point(10, 20)\nlet alice = Person(\"Alice\", 30)\n----\n\nEvery field gives an accessor of the same name. Called *without* an argument it reads; called *with* one it writes and returns the instance — which allows chaining:\n\n[source,golo]\n----\nprintln(p1: x()) # 10\nprintln(p1) # Point(x=10, y=20)\n\np1: x(100)\nprintln(p1) # Point(x=100, y=20)\n\nlet rect = Rectangle(0, 0)\n : width(50)\n : height(30)\nprintln(rect) # Rectangle(width=50, height=30)\n----\n\nThat last pattern is the *builder pattern*, for free: a neutral instance, then the fields that matter.\n------------------------------------------------------------------------\n2. Create, fill, read\n file: ebook/en/volume-1-fundamentals/chapters/09-dynamic-objects.adoc:7\n in: [en] volume-1-fundamentals \u003e Dynamic objects \u003e Create, fill, read\n matched: struct, create (score 3.8)\n\n[source,golo]\n----\nlet person = DynamicObject()\n : name(\"Alice\")\n : age(30)\n : city(\"Paris\")\n\nprintln(person: name()) # Alice\nprintln(person: age()) # 30\n----\n\nThe mechanism fits in one sentence: *`: name(value)` writes and returns the object; `: name()` reads*. It is the same accessor syntax as on a `struct`, except that no prior definition is needed — the property is born from its first write, and returning `this` makes the spelling chainable.\n------------------------------------------------------------------------\n3. Create, read, write\n file: ebook/en/volume-2-standard-library/chapters/08-observables.adoc:10\n in: [en] volume-2-standard-library \u003e Observables \u003e Create, read, write\n matched: create (score 2.5)\n\n[source,golo]\n----\nlet counter = observable(0)\nlet message = observable(\"Hello\")\n\nprintln(observableGet(counter)) # 0\n\nobservableSet(counter, 42)\nprintln(observableGet(counter)) # 42\n----\n\nSo far this is a variable with heavier syntax. What changes everything is subscription.\n------------------------------------------------------------------------\n4. Augmenting a struct\n file: ebook/en/volume-1-fundamentals/chapters/07-structs.adoc:85\n in: [en] volume-1-fundamentals \u003e Structs and augmentations \u003e Augmenting a struct\n matched: struct (score 2.3)\n\n`augment` adds methods to an existing struct. The first parameter is called `this` by convention and is the instance.\n\n[source,golo]\n----\nstruct Person = { name, age }\n\naugment Person {\n function greet = |this| -\u003e \"Hello, my name is \" + this: name()\n\n function isAdult = |this| -\u003e this: age() \u003e= 18\n\n function birthday = |this| {\n this: age(this: age() + 1)\n return this\n }\n}\n\nlet alice = Person(\"Alice\", 30)\nprintln(alice: greet()) # Hello, my name is Alice\nprintln(alice: isAdult()) # true\n\nalice: birthday(): birthday()\nprintln(alice: age()) # 32\n----\n\nReturning `this` from a method that modifies the instance makes it chainable, exactly like the generated accessors.\n------------------------------------------------------------------------\n5. The methods every struct already has\n file: ebook/en/volume-1-fundamentals/chapters/07-structs.adoc:37\n in: [en] volume-1-fundamentals \u003e Structs and augmentations \u003e The methods every struct already has\n matched: struct (score 2.3)\n\nWithout writing anything, every struct knows how to copy, compare and describe itself.\n\n[cols=\"2,3\",options=\"header\"]\n|===\n| Method | What it does\n| `copy()` | Mutable copy\n| `frozenCopy()` | Immutable copy\n| `isFrozen()` | Whether the instance is frozen\n| `equals(other)` / `hashCode()` | Structural equality\n| `compareTo(other)` | Comparison, field by field\n| `members()` / `values()` | Field names and values\n| `get(name)` / `set(name, val)` | Dynamic access by field name\n|===\n\n[source,golo]\n----\nlet alice = Person(\"Alice\", 30)\n\nlet bob = alice: copy(): name(\"Bob\"): age(28)\nprintln(alice) # Person(name=Alice, age=30) — unchanged\nprintln(bob) # Person(name=Bob, age=28)\n\nlet frozen = alice: frozenCopy()\nprintln(frozen: isFrozen()) # true\n# frozen: age(31) # Exception: cannot modify frozen struct instance\n----\n\nEquality is structural, and comparison follows field order — so here the name first, the age second:\n\n[source,golo]\n----\nprintln(Person(\"Alice\", 30): equals(Person(\"Alice\", 30))) # true\nprintln(Person(\"Alice\", 30) \u003c Person(\"Bob\", 25)) # true\nprintln(Person(\"Alice\", 30): compareTo(Person(\"Bob\", 25))) # -1\n----\n\nIntrospection gives access to fields by name, which makes generic code possible:\n\n[source,golo]\n----\nprintln(alice: members()) # [name, age]\nprintln(alice: values()) # [Alice, 30]\nprintln(alice: get(\"age\")) # 30\nalice: set(\"age\", 31)\n----\n", | ||
| 111 | + "ref": "LSHzFiHW6r8er4gXXq0PNEfD9juiAmEJ" | ||
| 112 | + } | ||
| 113 | + } | ||
| 114 | + ], | ||
| 115 | + "role": "tool" | ||
| 116 | + }, | ||
| 117 | + { | ||
| 118 | + "content": [ | ||
| 119 | + { | ||
| 120 | + "text": "In Golo, a **struct** is a user-defined data type that groups related fields together, similar to a class or record in other languages. It allows you to create instances with named fields and provides built-in methods for accessing, modifying, and inspecting the data.\n\n### Key Features of Structs in Golo:\n1. **Definition**: \n You define a struct using `struct Name = { field1, field2, ... }`. \n Example: \n ```golo\n struct Point = { x, y }\n ```\n\n2. **Instance Creation**: \n Create an instance by calling the struct name with values: \n ```golo\n let p1 = Point(10, 20)\n ```\n\n3. **Field Access**: \n - Read a field using `: fieldName()`: \n ```golo\n println(p1: x()) # 10\n ```\n - Write a field using `: fieldName(value)` (returns the instance for chaining): \n ```golo\n p1: x(100)\n println(p1: x()) # 100\n ```\n\n4. **Builder Pattern**: \n You can initialize a struct and then modify specific fields: \n ```golo\n let rect = Rectangle(0, 0)\n : width(50)\n : height(30)\n println(rect) # Rectangle(width=50, height=30)\n ```\n\n5. **Methods**: \n You can add methods to a struct using `augment`: \n ```golo\n augment Point {\n function distanceFromOrigin = |this| -\\u003e\n Math.sqrt(this: x() ** 2 + this: y() ** 2)\n }\n println(p1: distanceFromOrigin()) # Distance from origin\n ```\n\n6. **Built-in Methods**: \n Every struct has utility methods like: \n - `copy()` / `frozenCopy()` for cloning \n - `equals(other)` for comparison \n - `members()` / `values()` for introspection \n\nStructs in Golo are lightweight, immutable by default (unless modified), and designed for simplicity and ease of use in scripting and prototyping." | ||
| 121 | + } | ||
| 122 | + ], | ||
| 123 | + "role": "model" | ||
| 124 | + } | ||
| 125 | + ] | ||
| 126 | +} | ||
| \ No newline at end of file | \ No newline at end of file | ||
added
demos/hello-golo/.turbo-golo/acp.toml +14 -0 | new file mode 100644 | ||
| @@ -0,0 +1,14 @@ | ||
| 1 | +[[agent]] | |
| 2 | +name = "Bob (llama.cpp)" | |
| 3 | +command = "docker" | |
| 4 | +args = ["agent", "serve", "acp", ".turbo-golo/agent.yaml"] | |
| 5 | +env = { TELEMETRY_ENABLED = "false" } | |
| 6 | + | |
| 7 | +# The user's own agent, the one Zed runs as "mini-me": it speaks the protocol | |
| 8 | +# on stdin/stdout when started with -acp, and announces slash commands the | |
| 9 | +# editor lists when / is typed at the start of the box. | |
| 10 | +[[agent]] | |
| 11 | +name = "mini-me (llama.cpp)" | |
| 12 | +command = "mm" | |
| 13 | +args = ["-acp"] | |
| 14 | +env = { AGENT_CONFIG = "/Users/k33g/kDrive/Rickub/bots-garden/mini-me/agent.llamacpp.yaml" } | |
| new file mode 100644 | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | +[[agent]] | ||
| 2 | +name = "Bob (llama.cpp)" | ||
| 3 | +command = "docker" | ||
| 4 | +args = ["agent", "serve", "acp", ".turbo-golo/agent.yaml"] | ||
| 5 | +env = { TELEMETRY_ENABLED = "false" } | ||
| 6 | + | ||
| 7 | +# The user's own agent, the one Zed runs as "mini-me": it speaks the protocol | ||
| 8 | +# on stdin/stdout when started with -acp, and announces slash commands the | ||
| 9 | +# editor lists when / is typed at the start of the box. | ||
| 10 | +[[agent]] | ||
| 11 | +name = "mini-me (llama.cpp)" | ||
| 12 | +command = "mm" | ||
| 13 | +args = ["-acp"] | ||
| 14 | +env = { AGENT_CONFIG = "/Users/k33g/kDrive/Rickub/bots-garden/mini-me/agent.llamacpp.yaml" } | ||
added
demos/hello-golo/.turbo-golo/agent.yaml +29 -0 | new file mode 100644 | ||
| @@ -0,0 +1,29 @@ | ||
| 1 | +# /Users/k33g/CodeBerg/turbo-editors/turbo-go/acp-agent/agent.yaml | |
| 2 | +providers: | |
| 3 | + llamacpp: | |
| 4 | + api_type: openai_chatcompletions | |
| 5 | + base_url: http://host.docker.internal:8080/v1 | |
| 6 | + | |
| 7 | +models: | |
| 8 | + mellum2: | |
| 9 | + provider: llamacpp | |
| 10 | + model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M | |
| 11 | + #max_tokens: 8192 | |
| 12 | + temperature: 0.7 | |
| 13 | + provider_opts: | |
| 14 | + context_size: 262144 | |
| 15 | + | |
| 16 | +agents: | |
| 17 | + root: | |
| 18 | + model: mellum2 | |
| 19 | + description: A helpful AI assistant running on a local llama.cpp server | |
| 20 | + instruction: | | |
| 21 | + You name is Bob 🤓, you are a knowledgeable code assistant that helps users with various tasks. | |
| 22 | + Be helpful, accurate, and concise in your responses. | |
| 23 | + You have access to the local filesystem and shell: use these tools | |
| 24 | + welcome_message: | | |
| 25 | + 🤖 Local Assistant propulsed by **llama.cpp** 🦙 | |
| 26 | + | |
| 27 | + toolsets: | |
| 28 | + - type: filesystem | |
| 29 | + - type: shell | |
| new file mode 100644 | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | +# /Users/k33g/CodeBerg/turbo-editors/turbo-go/acp-agent/agent.yaml | ||
| 2 | +providers: | ||
| 3 | + llamacpp: | ||
| 4 | + api_type: openai_chatcompletions | ||
| 5 | + base_url: http://host.docker.internal:8080/v1 | ||
| 6 | + | ||
| 7 | +models: | ||
| 8 | + mellum2: | ||
| 9 | + provider: llamacpp | ||
| 10 | + model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M | ||
| 11 | + #max_tokens: 8192 | ||
| 12 | + temperature: 0.7 | ||
| 13 | + provider_opts: | ||
| 14 | + context_size: 262144 | ||
| 15 | + | ||
| 16 | +agents: | ||
| 17 | + root: | ||
| 18 | + model: mellum2 | ||
| 19 | + description: A helpful AI assistant running on a local llama.cpp server | ||
| 20 | + instruction: | | ||
| 21 | + You name is Bob 🤓, you are a knowledgeable code assistant that helps users with various tasks. | ||
| 22 | + Be helpful, accurate, and concise in your responses. | ||
| 23 | + You have access to the local filesystem and shell: use these tools | ||
| 24 | + welcome_message: | | ||
| 25 | + 🤖 Local Assistant propulsed by **llama.cpp** 🦙 | ||
| 26 | + | ||
| 27 | + toolsets: | ||
| 28 | + - type: filesystem | ||
| 29 | + - type: shell | ||
added
demos/hello-golo/.turbo-golo/settings.toml +18 -0 | new file mode 100644 | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +# turbo-golo project settings. | |
| 2 | +# | |
| 3 | +# These apply to everyone who opens this project in turbo-golo. Delete this | |
| 4 | +# file and the editor falls back to its own defaults. | |
| 5 | + | |
| 6 | +[editor] | |
| 7 | + | |
| 8 | +# The colour theme to start in. `turbo-golo -list-themes` lists them all. | |
| 9 | +# A -theme flag on the command line overrides this. | |
| 10 | +theme = "borland-light" | |
| 11 | + | |
| 12 | +# Write modified files by themselves, a short while after you stop typing. | |
| 13 | +# On, because a project that has gone to the trouble of having a settings file | |
| 14 | +# has said what it wants; set it to false and save, and it stops at once. | |
| 15 | +autosave = true | |
| 16 | + | |
| 17 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | |
| 18 | +autosave_delay = "2s" | |
| new file mode 100644 | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | +# turbo-golo project settings. | ||
| 2 | +# | ||
| 3 | +# These apply to everyone who opens this project in turbo-golo. Delete this | ||
| 4 | +# file and the editor falls back to its own defaults. | ||
| 5 | + | ||
| 6 | +[editor] | ||
| 7 | + | ||
| 8 | +# The colour theme to start in. `turbo-golo -list-themes` lists them all. | ||
| 9 | +# A -theme flag on the command line overrides this. | ||
| 10 | +theme = "borland-light" | ||
| 11 | + | ||
| 12 | +# Write modified files by themselves, a short while after you stop typing. | ||
| 13 | +# On, because a project that has gone to the trouble of having a settings file | ||
| 14 | +# has said what it wants; set it to false and save, and it stops at once. | ||
| 15 | +autosave = true | ||
| 16 | + | ||
| 17 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | ||
| 18 | +autosave_delay = "2s" | ||
added
demos/hello-golo/.turbo-golo/snippets.toml +145 -0 | new file mode 100644 | ||
| @@ -0,0 +1,145 @@ | ||
| 1 | +# turbo-golo snippets. | |
| 2 | +# | |
| 3 | +# Each [[snippet]] becomes one line of the Snippets menu. Snippets sharing a | |
| 4 | +# group appear together in a submenu of that name; one with no group goes into | |
| 5 | +# General. A snippet is inserted at the cursor, and every line after the | |
| 6 | +# first is indented to match the line you inserted it on. | |
| 7 | +# | |
| 8 | +# languages restricts a snippet to files of those kinds, by the names the | |
| 9 | +# editor uses: bash, dockerfile, golo, html, javascript, markdown, toml, xml, | |
| 10 | +# yaml. Leave it out and the snippet is offered everywhere. | |
| 11 | +# | |
| 12 | +# Bodies are indented with two spaces, which is what every example in the | |
| 13 | +# GoloScript documentation and its own templates use. Golo has no formatter to | |
| 14 | +# disagree with, so the convention is the only authority there is. | |
| 15 | +# | |
| 16 | +# Every Golo body below is written in single quotes — '''…''' rather than | |
| 17 | +# """…""" — because a Golo string carries \n and \" the way a Go string does, | |
| 18 | +# and TOML would interpret those escapes in a basic string before the editor | |
| 19 | +# ever saw them. In a literal string a backslash is just a backslash, which is | |
| 20 | +# what a Golo snippet needs. | |
| 21 | +# | |
| 22 | +# Your own snippets, shared across every project, go in: | |
| 23 | +# /Users/k33g/Library/Application Support/turbo-golo/snippets.toml | |
| 24 | + | |
| 25 | +[[snippet]] | |
| 26 | +name = "module" | |
| 27 | +group = "Golo" | |
| 28 | +languages = ["golo"] | |
| 29 | +body = ''' | |
| 30 | +module hello.World | |
| 31 | + | |
| 32 | +function main = |args| { | |
| 33 | + println("Hello, Golo!") | |
| 34 | +}''' | |
| 35 | + | |
| 36 | +[[snippet]] | |
| 37 | +name = "main" | |
| 38 | +group = "Golo" | |
| 39 | +languages = ["golo"] | |
| 40 | +body = ''' | |
| 41 | +function main = |args| { | |
| 42 | + println("Hello, Golo!") | |
| 43 | +}''' | |
| 44 | + | |
| 45 | +[[snippet]] | |
| 46 | +name = "function" | |
| 47 | +group = "Golo" | |
| 48 | +languages = ["golo"] | |
| 49 | +body = ''' | |
| 50 | +function name = |a, b| { | |
| 51 | + return a + b | |
| 52 | +}''' | |
| 53 | + | |
| 54 | +[[snippet]] | |
| 55 | +name = "closure" | |
| 56 | +group = "Golo" | |
| 57 | +languages = ["golo"] | |
| 58 | +body = ''' | |
| 59 | +let f = |x| -> x * 2''' | |
| 60 | + | |
| 61 | +[[snippet]] | |
| 62 | +name = "struct" | |
| 63 | +group = "Golo" | |
| 64 | +languages = ["golo"] | |
| 65 | +body = ''' | |
| 66 | +struct Point = { x, y }''' | |
| 67 | + | |
| 68 | +[[snippet]] | |
| 69 | +name = "union" | |
| 70 | +group = "Golo" | |
| 71 | +languages = ["golo"] | |
| 72 | +body = ''' | |
| 73 | +union Shape = { | |
| 74 | + Circle = { radius } | |
| 75 | + Rect = { width, height } | |
| 76 | +}''' | |
| 77 | + | |
| 78 | +[[snippet]] | |
| 79 | +name = "augment" | |
| 80 | +group = "Golo" | |
| 81 | +languages = ["golo"] | |
| 82 | +body = ''' | |
| 83 | +augment Point { | |
| 84 | + function describe = |this| -> "(" + this: x() + ", " + this: y() + ")" | |
| 85 | +}''' | |
| 86 | + | |
| 87 | +[[snippet]] | |
| 88 | +name = "match" | |
| 89 | +group = "Golo" | |
| 90 | +languages = ["golo"] | |
| 91 | +body = ''' | |
| 92 | +let label = match { | |
| 93 | + when n < 0 then "negative" | |
| 94 | + when n == 0 then "zero" | |
| 95 | + otherwise "positive" | |
| 96 | +}''' | |
| 97 | + | |
| 98 | +[[snippet]] | |
| 99 | +name = "foreach" | |
| 100 | +group = "Golo" | |
| 101 | +languages = ["golo"] | |
| 102 | +body = ''' | |
| 103 | +foreach item in list[1, 2, 3] { | |
| 104 | + println(item) | |
| 105 | +}''' | |
| 106 | + | |
| 107 | +[[snippet]] | |
| 108 | +name = "for" | |
| 109 | +group = "Golo" | |
| 110 | +languages = ["golo"] | |
| 111 | +body = ''' | |
| 112 | +for (var i = 0, i < 10, i = i + 1) { | |
| 113 | + println(i) | |
| 114 | +}''' | |
| 115 | + | |
| 116 | +[[snippet]] | |
| 117 | +name = "try" | |
| 118 | +group = "Golo" | |
| 119 | +languages = ["golo"] | |
| 120 | +body = ''' | |
| 121 | +try { | |
| 122 | + throw "boom" | |
| 123 | +} catch (e) { | |
| 124 | + println("caught: \"" + e + "\"") | |
| 125 | +} finally { | |
| 126 | + println("done") | |
| 127 | +}''' | |
| 128 | + | |
| 129 | +[[snippet]] | |
| 130 | +name = "comprehension" | |
| 131 | +group = "Golo" | |
| 132 | +languages = ["golo"] | |
| 133 | +body = ''' | |
| 134 | +let squares = list[x * x foreach x in range(1, 6) when x > 2]''' | |
| 135 | + | |
| 136 | +[[snippet]] | |
| 137 | +group = "General" | |
| 138 | +name = "Hello" | |
| 139 | +body = "Hello!!!" | |
| 140 | + | |
| 141 | +[[snippet]] | |
| 142 | +group = "Markdown" | |
| 143 | +name = "Image" | |
| 144 | +languages = ["markdown"] | |
| 145 | +body = "" | |
| new file mode 100644 | |||
| @@ -0,0 +1,145 @@ | |||
| 1 | +# turbo-golo snippets. | ||
| 2 | +# | ||
| 3 | +# Each [[snippet]] becomes one line of the Snippets menu. Snippets sharing a | ||
| 4 | +# group appear together in a submenu of that name; one with no group goes into | ||
| 5 | +# General. A snippet is inserted at the cursor, and every line after the | ||
| 6 | +# first is indented to match the line you inserted it on. | ||
| 7 | +# | ||
| 8 | +# languages restricts a snippet to files of those kinds, by the names the | ||
| 9 | +# editor uses: bash, dockerfile, golo, html, javascript, markdown, toml, xml, | ||
| 10 | +# yaml. Leave it out and the snippet is offered everywhere. | ||
| 11 | +# | ||
| 12 | +# Bodies are indented with two spaces, which is what every example in the | ||
| 13 | +# GoloScript documentation and its own templates use. Golo has no formatter to | ||
| 14 | +# disagree with, so the convention is the only authority there is. | ||
| 15 | +# | ||
| 16 | +# Every Golo body below is written in single quotes — '''…''' rather than | ||
| 17 | +# """…""" — because a Golo string carries \n and \" the way a Go string does, | ||
| 18 | +# and TOML would interpret those escapes in a basic string before the editor | ||
| 19 | +# ever saw them. In a literal string a backslash is just a backslash, which is | ||
| 20 | +# what a Golo snippet needs. | ||
| 21 | +# | ||
| 22 | +# Your own snippets, shared across every project, go in: | ||
| 23 | +# /Users/k33g/Library/Application Support/turbo-golo/snippets.toml | ||
| 24 | + | ||
| 25 | +[[snippet]] | ||
| 26 | +name = "module" | ||
| 27 | +group = "Golo" | ||
| 28 | +languages = ["golo"] | ||
| 29 | +body = ''' | ||
| 30 | +module hello.World | ||
| 31 | + | ||
| 32 | +function main = |args| { | ||
| 33 | + println("Hello, Golo!") | ||
| 34 | +}''' | ||
| 35 | + | ||
| 36 | +[[snippet]] | ||
| 37 | +name = "main" | ||
| 38 | +group = "Golo" | ||
| 39 | +languages = ["golo"] | ||
| 40 | +body = ''' | ||
| 41 | +function main = |args| { | ||
| 42 | + println("Hello, Golo!") | ||
| 43 | +}''' | ||
| 44 | + | ||
| 45 | +[[snippet]] | ||
| 46 | +name = "function" | ||
| 47 | +group = "Golo" | ||
| 48 | +languages = ["golo"] | ||
| 49 | +body = ''' | ||
| 50 | +function name = |a, b| { | ||
| 51 | + return a + b | ||
| 52 | +}''' | ||
| 53 | + | ||
| 54 | +[[snippet]] | ||
| 55 | +name = "closure" | ||
| 56 | +group = "Golo" | ||
| 57 | +languages = ["golo"] | ||
| 58 | +body = ''' | ||
| 59 | +let f = |x| -> x * 2''' | ||
| 60 | + | ||
| 61 | +[[snippet]] | ||
| 62 | +name = "struct" | ||
| 63 | +group = "Golo" | ||
| 64 | +languages = ["golo"] | ||
| 65 | +body = ''' | ||
| 66 | +struct Point = { x, y }''' | ||
| 67 | + | ||
| 68 | +[[snippet]] | ||
| 69 | +name = "union" | ||
| 70 | +group = "Golo" | ||
| 71 | +languages = ["golo"] | ||
| 72 | +body = ''' | ||
| 73 | +union Shape = { | ||
| 74 | + Circle = { radius } | ||
| 75 | + Rect = { width, height } | ||
| 76 | +}''' | ||
| 77 | + | ||
| 78 | +[[snippet]] | ||
| 79 | +name = "augment" | ||
| 80 | +group = "Golo" | ||
| 81 | +languages = ["golo"] | ||
| 82 | +body = ''' | ||
| 83 | +augment Point { | ||
| 84 | + function describe = |this| -> "(" + this: x() + ", " + this: y() + ")" | ||
| 85 | +}''' | ||
| 86 | + | ||
| 87 | +[[snippet]] | ||
| 88 | +name = "match" | ||
| 89 | +group = "Golo" | ||
| 90 | +languages = ["golo"] | ||
| 91 | +body = ''' | ||
| 92 | +let label = match { | ||
| 93 | + when n < 0 then "negative" | ||
| 94 | + when n == 0 then "zero" | ||
| 95 | + otherwise "positive" | ||
| 96 | +}''' | ||
| 97 | + | ||
| 98 | +[[snippet]] | ||
| 99 | +name = "foreach" | ||
| 100 | +group = "Golo" | ||
| 101 | +languages = ["golo"] | ||
| 102 | +body = ''' | ||
| 103 | +foreach item in list[1, 2, 3] { | ||
| 104 | + println(item) | ||
| 105 | +}''' | ||
| 106 | + | ||
| 107 | +[[snippet]] | ||
| 108 | +name = "for" | ||
| 109 | +group = "Golo" | ||
| 110 | +languages = ["golo"] | ||
| 111 | +body = ''' | ||
| 112 | +for (var i = 0, i < 10, i = i + 1) { | ||
| 113 | + println(i) | ||
| 114 | +}''' | ||
| 115 | + | ||
| 116 | +[[snippet]] | ||
| 117 | +name = "try" | ||
| 118 | +group = "Golo" | ||
| 119 | +languages = ["golo"] | ||
| 120 | +body = ''' | ||
| 121 | +try { | ||
| 122 | + throw "boom" | ||
| 123 | +} catch (e) { | ||
| 124 | + println("caught: \"" + e + "\"") | ||
| 125 | +} finally { | ||
| 126 | + println("done") | ||
| 127 | +}''' | ||
| 128 | + | ||
| 129 | +[[snippet]] | ||
| 130 | +name = "comprehension" | ||
| 131 | +group = "Golo" | ||
| 132 | +languages = ["golo"] | ||
| 133 | +body = ''' | ||
| 134 | +let squares = list[x * x foreach x in range(1, 6) when x > 2]''' | ||
| 135 | + | ||
| 136 | +[[snippet]] | ||
| 137 | +group = "General" | ||
| 138 | +name = "Hello" | ||
| 139 | +body = "Hello!!!" | ||
| 140 | + | ||
| 141 | +[[snippet]] | ||
| 142 | +group = "Markdown" | ||
| 143 | +name = "Image" | ||
| 144 | +languages = ["markdown"] | ||
| 145 | +body = "" | ||
added
demos/hello-golo/.turbo-golo/tools.toml +109 -0 | new file mode 100644 | ||
| @@ -0,0 +1,109 @@ | ||
| 1 | +# turbo-golo tools. | |
| 2 | +# | |
| 3 | +# Each [[tool]] becomes one line of the Golo menu, in the order they appear | |
| 4 | +# here. name is what the menu shows; a letter between tildes is its hot key, and | |
| 5 | +# no two tools should claim the same one. | |
| 6 | +# | |
| 7 | +# command goes to "sh -c", so pipes, globs and && work: one entry can be a | |
| 8 | +# whole sequence. | |
| 9 | +# | |
| 10 | +# menu says which menu it appears in. Leave it out and the tool goes into the | |
| 11 | +# Golo menu; name anything else and that menu is created for you, in the order | |
| 12 | +# the names first appear here. A tool that has nothing to do with Golo belongs | |
| 13 | +# in one of your own: | |
| 14 | +# | |
| 15 | +# [[tool]] | |
| 16 | +# name = "~E~cho" | |
| 17 | +# command = "echo TADA" | |
| 18 | +# menu = "Tools" | |
| 19 | +# | |
| 20 | +# A {{label}} in a command is a value the editor asks for before running it, in | |
| 21 | +# a box titled after the tool. The text between the braces is what it asks for: | |
| 22 | +# | |
| 23 | +# [[tool]] | |
| 24 | +# name = "~R~un" | |
| 25 | +# command = "golo {{script}}" | |
| 26 | +# | |
| 27 | +# The value is quoted, so a path with a space in it stays one argument. Add ... | |
| 28 | +# inside the braces when you mean several arguments rather than one value: | |
| 29 | +# | |
| 30 | +# [[tool]] | |
| 31 | +# name = "Run with ~a~rguments" | |
| 32 | +# command = "golo main.golo {{arguments...}}" | |
| 33 | +# | |
| 34 | +# Double braces, not single. Single ones appear in real commands — awk '{print | |
| 35 | +# $1}' and find . -exec rm {} + are both ordinary things to put here — and | |
| 36 | +# neither is asking you for anything. | |
| 37 | +# | |
| 38 | +# output says where what the command prints goes: | |
| 39 | +# popup a dialog that fills in as it runs, and says the exit code (default) | |
| 40 | +# terminal a terminal window, for anything that reads the keyboard or runs long | |
| 41 | +# editor an editing window once it has finished, to search with Ctrl-F | |
| 42 | +# | |
| 43 | +# Commands run in the directory the editor was started in, which is why they | |
| 44 | +# see the whole project when you start from its root. Golo has no project | |
| 45 | +# manifest: a script is a file, and every command here names the file it | |
| 46 | +# works on. | |
| 47 | + | |
| 48 | +[[tool]] | |
| 49 | +name = "~R~un" | |
| 50 | +# The interpreter, on the file you name. A terminal, not a popup: a script that | |
| 51 | +# reads the keyboard has to be able to be answered, and one that runs long has | |
| 52 | +# to be able to be interrupted. | |
| 53 | +command = "golo {{script, e.g. main.golo}}" | |
| 54 | +output = "terminal" | |
| 55 | + | |
| 56 | +[[tool]] | |
| 57 | +name = "~T~est" | |
| 58 | +# Every *_test.golo under the current directory, with gololang.Testing. | |
| 59 | +command = "golo --test" | |
| 60 | +output = "popup" | |
| 61 | + | |
| 62 | +[[tool]] | |
| 63 | +name = "Test ~o~ne" | |
| 64 | +command = "golo --test {{test file or directory}}" | |
| 65 | +output = "popup" | |
| 66 | + | |
| 67 | +[[tool]] | |
| 68 | +name = "~D~ebug" | |
| 69 | +# The same interpreter with its step debugger on. It reads the keyboard, so it | |
| 70 | +# needs a terminal. | |
| 71 | +command = "golo --debug {{script, e.g. main.golo}}" | |
| 72 | +output = "terminal" | |
| 73 | + | |
| 74 | +[[tool]] | |
| 75 | +name = "R~E~PL" | |
| 76 | +# golo with no file starts its read-eval-print loop. | |
| 77 | +command = "golo" | |
| 78 | +output = "terminal" | |
| 79 | + | |
| 80 | +[[tool]] | |
| 81 | +name = "~N~ew script" | |
| 82 | +# Writes a starter program from GoloScript's own template. | |
| 83 | +command = "golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}" | |
| 84 | +output = "popup" | |
| 85 | + | |
| 86 | +[[tool]] | |
| 87 | +name = "~B~uild native" | |
| 88 | +# gogolo transpiles the script to Go and compiles it to a native executable. | |
| 89 | +# It needs the Go toolchain on PATH. | |
| 90 | +command = "gogolo build -o {{output executable}} {{script, e.g. main.golo}}" | |
| 91 | +output = "popup" | |
| 92 | + | |
| 93 | +[[tool]] | |
| 94 | +name = "Build ~w~asm" | |
| 95 | +# wagolo transpiles the script to Go and compiles it to WebAssembly with | |
| 96 | +# TinyGo. wasi is the target a runtime such as wasmtime or Node runs; js and | |
| 97 | +# wasip2 are the others. | |
| 98 | +command = "wagolo build -target={{target: wasi, js or wasip2}} -o {{output .wasm}} {{script, e.g. main.golo}}" | |
| 99 | +output = "popup" | |
| 100 | + | |
| 101 | +# A tool naming a `menu` gets a menu of its own on the bar. Nothing above does, | |
| 102 | +# so every tool above is in the Golo menu. This one is in a menu called Tools, | |
| 103 | +# which appears between Golo and Help — that is the whole mechanism. | |
| 104 | + | |
| 105 | +[[tool]] | |
| 106 | +name = "~E~cho" | |
| 107 | +command = "echo 🎉 tada!" | |
| 108 | +menu = "Tools" | |
| 109 | +output = "terminal" | |
| new file mode 100644 | |||
| @@ -0,0 +1,109 @@ | |||
| 1 | +# turbo-golo tools. | ||
| 2 | +# | ||
| 3 | +# Each [[tool]] becomes one line of the Golo menu, in the order they appear | ||
| 4 | +# here. name is what the menu shows; a letter between tildes is its hot key, and | ||
| 5 | +# no two tools should claim the same one. | ||
| 6 | +# | ||
| 7 | +# command goes to "sh -c", so pipes, globs and && work: one entry can be a | ||
| 8 | +# whole sequence. | ||
| 9 | +# | ||
| 10 | +# menu says which menu it appears in. Leave it out and the tool goes into the | ||
| 11 | +# Golo menu; name anything else and that menu is created for you, in the order | ||
| 12 | +# the names first appear here. A tool that has nothing to do with Golo belongs | ||
| 13 | +# in one of your own: | ||
| 14 | +# | ||
| 15 | +# [[tool]] | ||
| 16 | +# name = "~E~cho" | ||
| 17 | +# command = "echo TADA" | ||
| 18 | +# menu = "Tools" | ||
| 19 | +# | ||
| 20 | +# A {{label}} in a command is a value the editor asks for before running it, in | ||
| 21 | +# a box titled after the tool. The text between the braces is what it asks for: | ||
| 22 | +# | ||
| 23 | +# [[tool]] | ||
| 24 | +# name = "~R~un" | ||
| 25 | +# command = "golo {{script}}" | ||
| 26 | +# | ||
| 27 | +# The value is quoted, so a path with a space in it stays one argument. Add ... | ||
| 28 | +# inside the braces when you mean several arguments rather than one value: | ||
| 29 | +# | ||
| 30 | +# [[tool]] | ||
| 31 | +# name = "Run with ~a~rguments" | ||
| 32 | +# command = "golo main.golo {{arguments...}}" | ||
| 33 | +# | ||
| 34 | +# Double braces, not single. Single ones appear in real commands — awk '{print | ||
| 35 | +# $1}' and find . -exec rm {} + are both ordinary things to put here — and | ||
| 36 | +# neither is asking you for anything. | ||
| 37 | +# | ||
| 38 | +# output says where what the command prints goes: | ||
| 39 | +# popup a dialog that fills in as it runs, and says the exit code (default) | ||
| 40 | +# terminal a terminal window, for anything that reads the keyboard or runs long | ||
| 41 | +# editor an editing window once it has finished, to search with Ctrl-F | ||
| 42 | +# | ||
| 43 | +# Commands run in the directory the editor was started in, which is why they | ||
| 44 | +# see the whole project when you start from its root. Golo has no project | ||
| 45 | +# manifest: a script is a file, and every command here names the file it | ||
| 46 | +# works on. | ||
| 47 | + | ||
| 48 | +[[tool]] | ||
| 49 | +name = "~R~un" | ||
| 50 | +# The interpreter, on the file you name. A terminal, not a popup: a script that | ||
| 51 | +# reads the keyboard has to be able to be answered, and one that runs long has | ||
| 52 | +# to be able to be interrupted. | ||
| 53 | +command = "golo {{script, e.g. main.golo}}" | ||
| 54 | +output = "terminal" | ||
| 55 | + | ||
| 56 | +[[tool]] | ||
| 57 | +name = "~T~est" | ||
| 58 | +# Every *_test.golo under the current directory, with gololang.Testing. | ||
| 59 | +command = "golo --test" | ||
| 60 | +output = "popup" | ||
| 61 | + | ||
| 62 | +[[tool]] | ||
| 63 | +name = "Test ~o~ne" | ||
| 64 | +command = "golo --test {{test file or directory}}" | ||
| 65 | +output = "popup" | ||
| 66 | + | ||
| 67 | +[[tool]] | ||
| 68 | +name = "~D~ebug" | ||
| 69 | +# The same interpreter with its step debugger on. It reads the keyboard, so it | ||
| 70 | +# needs a terminal. | ||
| 71 | +command = "golo --debug {{script, e.g. main.golo}}" | ||
| 72 | +output = "terminal" | ||
| 73 | + | ||
| 74 | +[[tool]] | ||
| 75 | +name = "R~E~PL" | ||
| 76 | +# golo with no file starts its read-eval-print loop. | ||
| 77 | +command = "golo" | ||
| 78 | +output = "terminal" | ||
| 79 | + | ||
| 80 | +[[tool]] | ||
| 81 | +name = "~N~ew script" | ||
| 82 | +# Writes a starter program from GoloScript's own template. | ||
| 83 | +command = "golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}" | ||
| 84 | +output = "popup" | ||
| 85 | + | ||
| 86 | +[[tool]] | ||
| 87 | +name = "~B~uild native" | ||
| 88 | +# gogolo transpiles the script to Go and compiles it to a native executable. | ||
| 89 | +# It needs the Go toolchain on PATH. | ||
| 90 | +command = "gogolo build -o {{output executable}} {{script, e.g. main.golo}}" | ||
| 91 | +output = "popup" | ||
| 92 | + | ||
| 93 | +[[tool]] | ||
| 94 | +name = "Build ~w~asm" | ||
| 95 | +# wagolo transpiles the script to Go and compiles it to WebAssembly with | ||
| 96 | +# TinyGo. wasi is the target a runtime such as wasmtime or Node runs; js and | ||
| 97 | +# wasip2 are the others. | ||
| 98 | +command = "wagolo build -target={{target: wasi, js or wasip2}} -o {{output .wasm}} {{script, e.g. main.golo}}" | ||
| 99 | +output = "popup" | ||
| 100 | + | ||
| 101 | +# A tool naming a `menu` gets a menu of its own on the bar. Nothing above does, | ||
| 102 | +# so every tool above is in the Golo menu. This one is in a menu called Tools, | ||
| 103 | +# which appears between Golo and Help — that is the whole mechanism. | ||
| 104 | + | ||
| 105 | +[[tool]] | ||
| 106 | +name = "~E~cho" | ||
| 107 | +command = "echo 🎉 tada!" | ||
| 108 | +menu = "Tools" | ||
| 109 | +output = "terminal" | ||
added
demos/hello-golo/hello.golo +7 -0 | new file mode 100644 | ||
| @@ -0,0 +1,7 @@ | ||
| 1 | +module hello.World | |
| 2 | + | |
| 3 | +# this is a demo | |
| 4 | + | |
| 5 | +function main = |args| { | |
| 6 | + println("Hello, Golo!") | |
| 7 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | +module hello.World | ||
| 2 | + | ||
| 3 | +# this is a demo | ||
| 4 | + | ||
| 5 | +function main = |args| { | ||
| 6 | + println("Hello, Golo!") | ||
| 7 | +} | ||
added
demos/hello/hello.golo +6 -0 | new file mode 100644 | ||
| @@ -0,0 +1,6 @@ | ||
| 1 | +module demo.Hello | |
| 2 | + | |
| 3 | +# The smallest Golo program: a module, a function, a greeting. | |
| 4 | +function main = |args| { | |
| 5 | + println("Hello, Golo! 👋") | |
| 6 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | +module demo.Hello | ||
| 2 | + | ||
| 3 | +# The smallest Golo program: a module, a function, a greeting. | ||
| 4 | +function main = |args| { | ||
| 5 | + println("Hello, Golo! 👋") | ||
| 6 | +} | ||
added
demos/shapes/shapes.golo +41 -0 | new file mode 100644 | ||
| @@ -0,0 +1,41 @@ | ||
| 1 | +module demo.Shapes | |
| 2 | + | |
| 3 | +# A struct is a named record. Its fields are read with a colon: p: x(). | |
| 4 | +struct Point = { x, y } | |
| 5 | + | |
| 6 | +# A union is one type with several forms. Each variant may carry fields, and | |
| 7 | +# every variant gets an is<Variant>() predicate for free. | |
| 8 | +union Shape = { | |
| 9 | + Circle = { radius } | |
| 10 | + Rect = { width, height } | |
| 11 | +} | |
| 12 | + | |
| 13 | +# An augmentation adds functions to a type after the fact. `this` is the value | |
| 14 | +# the function is called on. | |
| 15 | +augment Point { | |
| 16 | + function describe = |this| -> "(" + this: x() + ", " + this: y() + ")" | |
| 17 | +} | |
| 18 | + | |
| 19 | +augment Shape { | |
| 20 | + function area = |this| { | |
| 21 | + return match { | |
| 22 | + when this: isCircle() then 3.14159 * this: radius() * this: radius() | |
| 23 | + when this: isRect() then this: width() * this: height() | |
| 24 | + otherwise 0 | |
| 25 | + } | |
| 26 | + } | |
| 27 | +} | |
| 28 | + | |
| 29 | +function main = |args| { | |
| 30 | + let origin = Point(0, 0) | |
| 31 | + println("origin is " + origin: describe()) | |
| 32 | + | |
| 33 | + let shapes = list[Shape_Circle(1.0), Shape_Rect(2.0, 3.0)] | |
| 34 | + foreach shape in shapes { | |
| 35 | + println(shape + " has area " + shape: area()) | |
| 36 | + } | |
| 37 | + | |
| 38 | + # A closure is a function value: |parameters| -> expression. | |
| 39 | + let scaled = list[shape: area() * 2 foreach shape in shapes] | |
| 40 | + println("doubled: " + scaled) | |
| 41 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,41 @@ | |||
| 1 | +module demo.Shapes | ||
| 2 | + | ||
| 3 | +# A struct is a named record. Its fields are read with a colon: p: x(). | ||
| 4 | +struct Point = { x, y } | ||
| 5 | + | ||
| 6 | +# A union is one type with several forms. Each variant may carry fields, and | ||
| 7 | +# every variant gets an is<Variant>() predicate for free. | ||
| 8 | +union Shape = { | ||
| 9 | + Circle = { radius } | ||
| 10 | + Rect = { width, height } | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +# An augmentation adds functions to a type after the fact. `this` is the value | ||
| 14 | +# the function is called on. | ||
| 15 | +augment Point { | ||
| 16 | + function describe = |this| -> "(" + this: x() + ", " + this: y() + ")" | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +augment Shape { | ||
| 20 | + function area = |this| { | ||
| 21 | + return match { | ||
| 22 | + when this: isCircle() then 3.14159 * this: radius() * this: radius() | ||
| 23 | + when this: isRect() then this: width() * this: height() | ||
| 24 | + otherwise 0 | ||
| 25 | + } | ||
| 26 | + } | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +function main = |args| { | ||
| 30 | + let origin = Point(0, 0) | ||
| 31 | + println("origin is " + origin: describe()) | ||
| 32 | + | ||
| 33 | + let shapes = list[Shape_Circle(1.0), Shape_Rect(2.0, 3.0)] | ||
| 34 | + foreach shape in shapes { | ||
| 35 | + println(shape + " has area " + shape: area()) | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + # A closure is a function value: |parameters| -> expression. | ||
| 39 | + let scaled = list[shape: area() * 2 foreach shape in shapes] | ||
| 40 | + println("doubled: " + scaled) | ||
| 41 | +} | ||
added
demos/shapes/shapes_test.golo +18 -0 | new file mode 100644 | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +module demo.ShapesTest | |
| 2 | + | |
| 3 | +# Run with `golo --test demos/shapes` — every *_test.golo in the directory — | |
| 4 | +# or pick this one file. The assertions come from the embedded testing module. | |
| 5 | +import gololang.Testing | |
| 6 | + | |
| 7 | +struct Point = { x, y } | |
| 8 | + | |
| 9 | +augment Point { | |
| 10 | + function describe = |this| -> "(" + this: x() + ", " + this: y() + ")" | |
| 11 | +} | |
| 12 | + | |
| 13 | +function main = |args| { | |
| 14 | + let p = Point(1, 2) | |
| 15 | + assertEqual(p: describe(), "(1, 2)", "a point describes itself") | |
| 16 | + assertEqual(p: x() + p: y(), 3, "fields add up") | |
| 17 | + assertTrue(p: x() < p: y(), "x comes before y") | |
| 18 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | +module demo.ShapesTest | ||
| 2 | + | ||
| 3 | +# Run with `golo --test demos/shapes` — every *_test.golo in the directory — | ||
| 4 | +# or pick this one file. The assertions come from the embedded testing module. | ||
| 5 | +import gololang.Testing | ||
| 6 | + | ||
| 7 | +struct Point = { x, y } | ||
| 8 | + | ||
| 9 | +augment Point { | ||
| 10 | + function describe = |this| -> "(" + this: x() + ", " + this: y() + ")" | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +function main = |args| { | ||
| 14 | + let p = Point(1, 2) | ||
| 15 | + assertEqual(p: describe(), "(1, 2)", "a point describes itself") | ||
| 16 | + assertEqual(p: x() + p: y(), 3, "fields add up") | ||
| 17 | + assertTrue(p: x() < p: y(), "x comes before y") | ||
| 18 | +} | ||
added
demos/syntax-tour/lexer-only.golo +26 -0 | new file mode 100644 | ||
| @@ -0,0 +1,26 @@ | ||
| 1 | +module demo.LexerOnly | |
| 2 | + | |
| 3 | +# This file is coloured and does NOT run. GoloScript's lexer reads every token | |
| 4 | +# below — token/token.go and lexer/lexer.go name them — and its parser then | |
| 5 | +# refuses them, as of v0.1.1. Turbo Golo colours what the lexer reads, so a | |
| 6 | +# coloured token is not a promise that the interpreter will accept it. | |
| 7 | + | |
| 8 | +# The local keyword: read as LOCAL, refused by the parser in a declaration. | |
| 9 | +local function hidden = |x| -> x + 1 | |
| 10 | + | |
| 11 | +function main = |args| { | |
| 12 | + # The long and float suffixes: read as numbers, refused by the parser. | |
| 13 | + let long = 42L | |
| 14 | + let float = 3.14F | |
| 15 | + let small = 2.0f | |
| 16 | + | |
| 17 | + # A character literal: read as CHAR, refused by the parser. | |
| 18 | + let c = 'x' | |
| 19 | + | |
| 20 | + # The range operator: read as .., refused by the parser. | |
| 21 | + let r = 1..3 | |
| 22 | + | |
| 23 | + # Two word operators the parser does not yet place in an expression. | |
| 24 | + let v = null orIfNull 0 | |
| 25 | + let t = c oftype String.class | |
| 26 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | +module demo.LexerOnly | ||
| 2 | + | ||
| 3 | +# This file is coloured and does NOT run. GoloScript's lexer reads every token | ||
| 4 | +# below — token/token.go and lexer/lexer.go name them — and its parser then | ||
| 5 | +# refuses them, as of v0.1.1. Turbo Golo colours what the lexer reads, so a | ||
| 6 | +# coloured token is not a promise that the interpreter will accept it. | ||
| 7 | + | ||
| 8 | +# The local keyword: read as LOCAL, refused by the parser in a declaration. | ||
| 9 | +local function hidden = |x| -> x + 1 | ||
| 10 | + | ||
| 11 | +function main = |args| { | ||
| 12 | + # The long and float suffixes: read as numbers, refused by the parser. | ||
| 13 | + let long = 42L | ||
| 14 | + let float = 3.14F | ||
| 15 | + let small = 2.0f | ||
| 16 | + | ||
| 17 | + # A character literal: read as CHAR, refused by the parser. | ||
| 18 | + let c = 'x' | ||
| 19 | + | ||
| 20 | + # The range operator: read as .., refused by the parser. | ||
| 21 | + let r = 1..3 | ||
| 22 | + | ||
| 23 | + # Two word operators the parser does not yet place in an expression. | ||
| 24 | + let v = null orIfNull 0 | ||
| 25 | + let t = c oftype String.class | ||
| 26 | +} | ||
added
demos/syntax-tour/tour.golo +118 -0 | new file mode 100644 | ||
| @@ -0,0 +1,118 @@ | ||
| 1 | +#!/usr/bin/env golo | |
| 2 | +module demo.Tour | |
| 3 | + | |
| 4 | +# A tour of what Turbo Golo colours, one construct per line or two. Every line | |
| 5 | +# here runs: `golo demos/syntax-tour/tour.golo` prints its way through. What the | |
| 6 | +# lexer reads but the interpreter's parser then refuses is in lexer-only.golo | |
| 7 | +# beside this file, which is coloured but does not run. | |
| 8 | + | |
| 9 | +import gololang.Errors | |
| 10 | + | |
| 11 | +---- | |
| 12 | +A block comment runs from four dashes to the next four, over as many lines as | |
| 13 | +it likes, with --- near misses and "quotes" inside it left alone. | |
| 14 | +---- | |
| 15 | + | |
| 16 | +# Structs, unions and augmentations are capitalised by convention, and the | |
| 17 | +# convention is what the colour follows. | |
| 18 | +struct Point = { x, y } | |
| 19 | + | |
| 20 | +union Shape = { | |
| 21 | + Circle = { radius } | |
| 22 | + Rect = { width, height } | |
| 23 | +} | |
| 24 | + | |
| 25 | +augment Shape$Circle { | |
| 26 | + function diameter = |this| -> this: radius() * 2 | |
| 27 | +} | |
| 28 | + | |
| 29 | +# A declared name is a function even though no parenthesis follows it. | |
| 30 | +function twice = |x| -> x * 2 | |
| 31 | + | |
| 32 | +function hidden = |x| { | |
| 33 | + return x + 1 | |
| 34 | +} | |
| 35 | + | |
| 36 | +function main = |args| { | |
| 37 | + # Numbers: integers, doubles, exponents. | |
| 38 | + let count = 42 | |
| 39 | + let ratio = 3.14 | |
| 40 | + let small = 1.5e-3 | |
| 41 | + let big = 2E10 | |
| 42 | + | |
| 43 | + # Strings, escapes, a multi-line string, and a hash that is not a comment. | |
| 44 | + let greeting = "Hello, \"Golo\"\n" | |
| 45 | + let hash = "# not a comment" | |
| 46 | + let dashes = "---- not a comment ----" | |
| 47 | + let multi = """ | |
| 48 | + a multi-line "string" | |
| 49 | + with "quotes" left alone | |
| 50 | + """ | |
| 51 | + | |
| 52 | + # Builtins are the interpreter's own functions; DynamicObject is one too. | |
| 53 | + println(greeting + hash + dashes) | |
| 54 | + println(multi) | |
| 55 | + println(str(count) + " " + str(ratio) + " " + str(small) + " " + str(big)) | |
| 56 | + let bag = DynamicObject() | |
| 57 | + bag: name("Golo") | |
| 58 | + println(bag: name()) | |
| 59 | + | |
| 60 | + # Collections and comprehensions. | |
| 61 | + let xs = list[1, 2, 3] | |
| 62 | + let squares = list[x * x foreach x in range(1, 6) when x > 2] | |
| 63 | + let pairs = map[["one", 1], ["two", 2]] | |
| 64 | + println(xs + " " + squares + " " + pairs) | |
| 65 | + | |
| 66 | + # Control flow: if, while, for, foreach, match. | |
| 67 | + var i = 0 | |
| 68 | + while i < 2 { | |
| 69 | + i = i + 1 | |
| 70 | + } | |
| 71 | + for (var j = 0, j < 2, j = j + 1) { | |
| 72 | + println("for " + j) | |
| 73 | + } | |
| 74 | + foreach x in xs { | |
| 75 | + println("foreach " + x) | |
| 76 | + } | |
| 77 | + let label = match { | |
| 78 | + when count < 0 then "negative" | |
| 79 | + when count == 0 then "zero" | |
| 80 | + otherwise "positive" | |
| 81 | + } | |
| 82 | + println(label) | |
| 83 | + if count > 40 and not (count is null) { | |
| 84 | + println("big enough") | |
| 85 | + } else { | |
| 86 | + println("small") | |
| 87 | + } | |
| 88 | + | |
| 89 | + # Structs, unions, augmentations, closures. | |
| 90 | + let p = Point(1, 2) | |
| 91 | + let c = Shape_Circle(2.0) | |
| 92 | + println(p: x() + p: y()) | |
| 93 | + println(c: isCircle() + " " + c: diameter()) | |
| 94 | + let f = |a, b| -> a + b | |
| 95 | + println(f(twice(20), hidden(1))) | |
| 96 | + | |
| 97 | + # Errors, and the Option union from gololang.Errors — Some is a variant, | |
| 98 | + # not a builtin, so it takes the colour every capitalised name does. | |
| 99 | + try { | |
| 100 | + throw "boom" | |
| 101 | + } catch (e) { | |
| 102 | + println("caught: " + e) | |
| 103 | + } finally { | |
| 104 | + println("done") | |
| 105 | + } | |
| 106 | + let maybe = Some(1) | |
| 107 | + println(maybe: isSome()) | |
| 108 | + | |
| 109 | + # Names may be any Unicode letter, or an emoji. | |
| 110 | + let été = "summer" | |
| 111 | + let 😀 = "smile" | |
| 112 | + println(été + " " + 😀) | |
| 113 | + | |
| 114 | + # Safe navigation on a null. | |
| 115 | + let nothing = null | |
| 116 | + println(nothing?: x()) | |
| 117 | +} | |
| 118 | + | |
| new file mode 100644 | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | +#!/usr/bin/env golo | ||
| 2 | +module demo.Tour | ||
| 3 | + | ||
| 4 | +# A tour of what Turbo Golo colours, one construct per line or two. Every line | ||
| 5 | +# here runs: `golo demos/syntax-tour/tour.golo` prints its way through. What the | ||
| 6 | +# lexer reads but the interpreter's parser then refuses is in lexer-only.golo | ||
| 7 | +# beside this file, which is coloured but does not run. | ||
| 8 | + | ||
| 9 | +import gololang.Errors | ||
| 10 | + | ||
| 11 | +---- | ||
| 12 | +A block comment runs from four dashes to the next four, over as many lines as | ||
| 13 | +it likes, with --- near misses and "quotes" inside it left alone. | ||
| 14 | +---- | ||
| 15 | + | ||
| 16 | +# Structs, unions and augmentations are capitalised by convention, and the | ||
| 17 | +# convention is what the colour follows. | ||
| 18 | +struct Point = { x, y } | ||
| 19 | + | ||
| 20 | +union Shape = { | ||
| 21 | + Circle = { radius } | ||
| 22 | + Rect = { width, height } | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +augment Shape$Circle { | ||
| 26 | + function diameter = |this| -> this: radius() * 2 | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +# A declared name is a function even though no parenthesis follows it. | ||
| 30 | +function twice = |x| -> x * 2 | ||
| 31 | + | ||
| 32 | +function hidden = |x| { | ||
| 33 | + return x + 1 | ||
| 34 | +} | ||
| 35 | + | ||
| 36 | +function main = |args| { | ||
| 37 | + # Numbers: integers, doubles, exponents. | ||
| 38 | + let count = 42 | ||
| 39 | + let ratio = 3.14 | ||
| 40 | + let small = 1.5e-3 | ||
| 41 | + let big = 2E10 | ||
| 42 | + | ||
| 43 | + # Strings, escapes, a multi-line string, and a hash that is not a comment. | ||
| 44 | + let greeting = "Hello, \"Golo\"\n" | ||
| 45 | + let hash = "# not a comment" | ||
| 46 | + let dashes = "---- not a comment ----" | ||
| 47 | + let multi = """ | ||
| 48 | + a multi-line "string" | ||
| 49 | + with "quotes" left alone | ||
| 50 | + """ | ||
| 51 | + | ||
| 52 | + # Builtins are the interpreter's own functions; DynamicObject is one too. | ||
| 53 | + println(greeting + hash + dashes) | ||
| 54 | + println(multi) | ||
| 55 | + println(str(count) + " " + str(ratio) + " " + str(small) + " " + str(big)) | ||
| 56 | + let bag = DynamicObject() | ||
| 57 | + bag: name("Golo") | ||
| 58 | + println(bag: name()) | ||
| 59 | + | ||
| 60 | + # Collections and comprehensions. | ||
| 61 | + let xs = list[1, 2, 3] | ||
| 62 | + let squares = list[x * x foreach x in range(1, 6) when x > 2] | ||
| 63 | + let pairs = map[["one", 1], ["two", 2]] | ||
| 64 | + println(xs + " " + squares + " " + pairs) | ||
| 65 | + | ||
| 66 | + # Control flow: if, while, for, foreach, match. | ||
| 67 | + var i = 0 | ||
| 68 | + while i < 2 { | ||
| 69 | + i = i + 1 | ||
| 70 | + } | ||
| 71 | + for (var j = 0, j < 2, j = j + 1) { | ||
| 72 | + println("for " + j) | ||
| 73 | + } | ||
| 74 | + foreach x in xs { | ||
| 75 | + println("foreach " + x) | ||
| 76 | + } | ||
| 77 | + let label = match { | ||
| 78 | + when count < 0 then "negative" | ||
| 79 | + when count == 0 then "zero" | ||
| 80 | + otherwise "positive" | ||
| 81 | + } | ||
| 82 | + println(label) | ||
| 83 | + if count > 40 and not (count is null) { | ||
| 84 | + println("big enough") | ||
| 85 | + } else { | ||
| 86 | + println("small") | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + # Structs, unions, augmentations, closures. | ||
| 90 | + let p = Point(1, 2) | ||
| 91 | + let c = Shape_Circle(2.0) | ||
| 92 | + println(p: x() + p: y()) | ||
| 93 | + println(c: isCircle() + " " + c: diameter()) | ||
| 94 | + let f = |a, b| -> a + b | ||
| 95 | + println(f(twice(20), hidden(1))) | ||
| 96 | + | ||
| 97 | + # Errors, and the Option union from gololang.Errors — Some is a variant, | ||
| 98 | + # not a builtin, so it takes the colour every capitalised name does. | ||
| 99 | + try { | ||
| 100 | + throw "boom" | ||
| 101 | + } catch (e) { | ||
| 102 | + println("caught: " + e) | ||
| 103 | + } finally { | ||
| 104 | + println("done") | ||
| 105 | + } | ||
| 106 | + let maybe = Some(1) | ||
| 107 | + println(maybe: isSome()) | ||
| 108 | + | ||
| 109 | + # Names may be any Unicode letter, or an emoji. | ||
| 110 | + let été = "summer" | ||
| 111 | + let 😀 = "smile" | ||
| 112 | + println(été + " " + 😀) | ||
| 113 | + | ||
| 114 | + # Safe navigation on a null. | ||
| 115 | + let nothing = null | ||
| 116 | + println(nothing?: x()) | ||
| 117 | +} | ||
| 118 | + | ||
added
diagram_test.go +202 -0 | new file mode 100644 | ||
| @@ -0,0 +1,202 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "encoding/xml" | |
| 5 | + "html" | |
| 6 | + "os" | |
| 7 | + "os/exec" | |
| 8 | + "regexp" | |
| 9 | + "sort" | |
| 10 | + "strings" | |
| 11 | + "testing" | |
| 12 | +) | |
| 13 | + | |
| 14 | +// The package diagram is drawn by hand and read by people, so nothing in the | |
| 15 | +// build notices when it stops describing the code. It started as Turbo Rust's | |
| 16 | +// and shipped naming internal/rustlang and "the Rust scanner" — an error no | |
| 17 | +// test could see, because a diagram is a file nothing imports. | |
| 18 | +// | |
| 19 | +// These tests hold it to `go list`: the boxes are the packages this module | |
| 20 | +// actually imports, and the arrows between the two boxes that are ours are the | |
| 21 | +// imports that really exist. This diagram began as Turbo MoonBit's, which is | |
| 22 | +// exactly the provenance the four checks below exist to catch. | |
| 23 | + | |
| 24 | +// diagramFile is the drawio the documentation links to. | |
| 25 | +const diagramFile = "docs/diagrams/packages.drawio" | |
| 26 | + | |
| 27 | +// mxFile is as much of drawio's format as these tests need: every cell, with | |
| 28 | +// its label, and — for an arrow — the two cells it joins. | |
| 29 | +type mxFile struct { | |
| 30 | + Host string `xml:"host,attr"` | |
| 31 | + Cells []mxCell `xml:"diagram>mxGraphModel>root>mxCell"` | |
| 32 | +} | |
| 33 | + | |
| 34 | +type mxCell struct { | |
| 35 | + ID string `xml:"id,attr"` | |
| 36 | + Value string `xml:"value,attr"` | |
| 37 | + Edge string `xml:"edge,attr"` | |
| 38 | + Source string `xml:"source,attr"` | |
| 39 | + Target string `xml:"target,attr"` | |
| 40 | +} | |
| 41 | + | |
| 42 | +// boldLabel is the package name inside a box: drawio stores the label as | |
| 43 | +// escaped HTML, and the name is the part in bold. | |
| 44 | +var boldLabel = regexp.MustCompile(`(?s)<b>(.*?)</b>`) | |
| 45 | + | |
| 46 | +// readDiagram parses the diagram, failing the test rather than returning an | |
| 47 | +// error — a diagram that will not parse is not a case any caller can handle. | |
| 48 | +func readDiagram(t *testing.T) mxFile { | |
| 49 | + t.Helper() | |
| 50 | + | |
| 51 | + raw, err := os.ReadFile(diagramFile) | |
| 52 | + if err != nil { | |
| 53 | + t.Fatalf("reading %s: %v", diagramFile, err) | |
| 54 | + } | |
| 55 | + | |
| 56 | + var file mxFile | |
| 57 | + if err := xml.Unmarshal(raw, &file); err != nil { | |
| 58 | + t.Fatalf("parsing %s: %v", diagramFile, err) | |
| 59 | + } | |
| 60 | + return file | |
| 61 | +} | |
| 62 | + | |
| 63 | +// boxes maps each box's package name to the id the arrows use for it. | |
| 64 | +func boxes(t *testing.T, file mxFile) map[string]string { | |
| 65 | + t.Helper() | |
| 66 | + | |
| 67 | + found := map[string]string{} | |
| 68 | + for _, cell := range file.Cells { | |
| 69 | + if cell.Edge == "1" || cell.Value == "" { | |
| 70 | + continue | |
| 71 | + } | |
| 72 | + label := html.UnescapeString(cell.Value) | |
| 73 | + // A box's package name is the part in bold, where there is one; the | |
| 74 | + // third-party box carries its name plain, with nothing to tell apart | |
| 75 | + // from it. | |
| 76 | + if match := boldLabel.FindStringSubmatch(label); match != nil { | |
| 77 | + label = match[1] | |
| 78 | + } | |
| 79 | + found[label] = cell.ID | |
| 80 | + } | |
| 81 | + return found | |
| 82 | +} | |
| 83 | + | |
| 84 | +// imports asks the toolchain what a package imports, shortened to the names the | |
| 85 | +// diagram uses: the last element for a turbo-core package, the module-relative | |
| 86 | +// path for one of ours, and "tcell/v2" for the one third-party dependency. | |
| 87 | +func imports(t *testing.T, pkg string) []string { | |
| 88 | + t.Helper() | |
| 89 | + | |
| 90 | + out, err := exec.Command("go", "list", "-f", `{{join .Imports "\n"}}`, pkg).Output() | |
| 91 | + if err != nil { | |
| 92 | + t.Fatalf("go list %s: %v", pkg, err) | |
| 93 | + } | |
| 94 | + | |
| 95 | + var names []string | |
| 96 | + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { | |
| 97 | + switch { | |
| 98 | + case strings.HasPrefix(line, "rickub.com/turbo-editors/turbo-core/"): | |
| 99 | + names = append(names, strings.TrimPrefix(line, "rickub.com/turbo-editors/turbo-core/")) | |
| 100 | + case strings.HasPrefix(line, "rickub.com/turbo-editors/turbo-golo/"): | |
| 101 | + names = append(names, strings.TrimPrefix(line, "rickub.com/turbo-editors/turbo-golo/")) | |
| 102 | + case strings.HasPrefix(line, "github.com/gdamore/tcell/"): | |
| 103 | + names = append(names, "tcell/v2") | |
| 104 | + } | |
| 105 | + } | |
| 106 | + sort.Strings(names) | |
| 107 | + return names | |
| 108 | +} | |
| 109 | + | |
| 110 | +// The boxes are exactly the packages the two packages of this module import, | |
| 111 | +// plus the two packages themselves. A box for a package nothing imports is as | |
| 112 | +// wrong as a missing one: both tell a reader something untrue about the code. | |
| 113 | +func TestTheDiagramDrawsExactlyThePackagesThisModuleImports(t *testing.T) { | |
| 114 | + drawn := boxes(t, readDiagram(t)) | |
| 115 | + | |
| 116 | + want := map[string]bool{"main": true, "internal/gololang": true} | |
| 117 | + for _, pkg := range append(imports(t, "."), imports(t, "./internal/gololang")...) { | |
| 118 | + want[pkg] = true | |
| 119 | + } | |
| 120 | + | |
| 121 | + for name := range want { | |
| 122 | + if _, ok := drawn[name]; !ok { | |
| 123 | + t.Errorf("%s draws no box for %q", diagramFile, name) | |
| 124 | + } | |
| 125 | + } | |
| 126 | + for name := range drawn { | |
| 127 | + if !want[name] { | |
| 128 | + t.Errorf("%s draws a box for %q, which nothing in this module imports", diagramFile, name) | |
| 129 | + } | |
| 130 | + } | |
| 131 | +} | |
| 132 | + | |
| 133 | +// Every arrow leaving one of our two boxes is an import that exists. This is | |
| 134 | +// the half that caught the copied diagram: an arrow drawn out of a box labelled | |
| 135 | +// internal/rustlang cannot be checked at all until the box is named right. | |
| 136 | +func TestEveryArrowOutOfOurPackagesIsARealImport(t *testing.T) { | |
| 137 | + file := readDiagram(t) | |
| 138 | + drawn := boxes(t, file) | |
| 139 | + | |
| 140 | + byID := map[string]string{} | |
| 141 | + for name, id := range drawn { | |
| 142 | + byID[id] = name | |
| 143 | + } | |
| 144 | + | |
| 145 | + ours := map[string]string{"main": ".", "internal/gololang": "./internal/gololang"} | |
| 146 | + for _, cell := range file.Cells { | |
| 147 | + if cell.Edge != "1" { | |
| 148 | + continue | |
| 149 | + } | |
| 150 | + from, ok := byID[cell.Source] | |
| 151 | + if !ok { | |
| 152 | + t.Errorf("%s draws an arrow out of unknown cell %q", diagramFile, cell.Source) | |
| 153 | + continue | |
| 154 | + } | |
| 155 | + pkg, ok := ours[from] | |
| 156 | + if !ok { | |
| 157 | + continue | |
| 158 | + } | |
| 159 | + | |
| 160 | + to := byID[cell.Target] | |
| 161 | + if to == "internal/gololang" && from == "main" { | |
| 162 | + continue // main imports it under its full path, already shortened | |
| 163 | + } | |
| 164 | + if !slicesContain(imports(t, pkg), to) { | |
| 165 | + t.Errorf("%s draws %s → %s, but %s imports no such package", diagramFile, from, to, from) | |
| 166 | + } | |
| 167 | + } | |
| 168 | +} | |
| 169 | + | |
| 170 | +// The file's host attribute names the project it was drawn for. It is the one | |
| 171 | +// field a reader never sees and a copy always keeps. | |
| 172 | +func TestTheDiagramSaysWhichProjectItWasDrawnFor(t *testing.T) { | |
| 173 | + if host := readDiagram(t).Host; host != "turbo-golo" { | |
| 174 | + t.Errorf("%s was drawn for %q, not turbo-golo", diagramFile, host) | |
| 175 | + } | |
| 176 | +} | |
| 177 | + | |
| 178 | +// No label anywhere in the diagram names another editor in the family, or the | |
| 179 | +// language it edits. The copied diagram said "the Rust scanner" in prose that | |
| 180 | +// no identifier check would have looked at. | |
| 181 | +func TestNoLabelInTheDiagramNamesAnotherEditorsLanguage(t *testing.T) { | |
| 182 | + for _, cell := range readDiagram(t).Cells { | |
| 183 | + label := html.UnescapeString(cell.Value) | |
| 184 | + for _, other := range []string{"moonbitlang", "pythonlang", "rustlang", "golang", "MoonBit", "Python", "Rust", "Go ", "turbo-moonbit", "turbo-python", "turbo-rust", "turbo-go"} { | |
| 185 | + if strings.Contains(label, other) { | |
| 186 | + t.Errorf("%s labels a cell %q, which names %q", diagramFile, label, other) | |
| 187 | + } | |
| 188 | + } | |
| 189 | + } | |
| 190 | +} | |
| 191 | + | |
| 192 | +// slicesContain says whether a sorted list holds a value. It is here rather | |
| 193 | +// than from the standard library's slices package so the test reads the same | |
| 194 | +// way in a checkout of any Go version this module supports. | |
| 195 | +func slicesContain(list []string, want string) bool { | |
| 196 | + for _, got := range list { | |
| 197 | + if got == want { | |
| 198 | + return true | |
| 199 | + } | |
| 200 | + } | |
| 201 | + return false | |
| 202 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,202 @@ | |||
| 1 | +package main | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "encoding/xml" | ||
| 5 | + "html" | ||
| 6 | + "os" | ||
| 7 | + "os/exec" | ||
| 8 | + "regexp" | ||
| 9 | + "sort" | ||
| 10 | + "strings" | ||
| 11 | + "testing" | ||
| 12 | +) | ||
| 13 | + | ||
| 14 | +// The package diagram is drawn by hand and read by people, so nothing in the | ||
| 15 | +// build notices when it stops describing the code. It started as Turbo Rust's | ||
| 16 | +// and shipped naming internal/rustlang and "the Rust scanner" — an error no | ||
| 17 | +// test could see, because a diagram is a file nothing imports. | ||
| 18 | +// | ||
| 19 | +// These tests hold it to `go list`: the boxes are the packages this module | ||
| 20 | +// actually imports, and the arrows between the two boxes that are ours are the | ||
| 21 | +// imports that really exist. This diagram began as Turbo MoonBit's, which is | ||
| 22 | +// exactly the provenance the four checks below exist to catch. | ||
| 23 | + | ||
| 24 | +// diagramFile is the drawio the documentation links to. | ||
| 25 | +const diagramFile = "docs/diagrams/packages.drawio" | ||
| 26 | + | ||
| 27 | +// mxFile is as much of drawio's format as these tests need: every cell, with | ||
| 28 | +// its label, and — for an arrow — the two cells it joins. | ||
| 29 | +type mxFile struct { | ||
| 30 | + Host string `xml:"host,attr"` | ||
| 31 | + Cells []mxCell `xml:"diagram>mxGraphModel>root>mxCell"` | ||
| 32 | +} | ||
| 33 | + | ||
| 34 | +type mxCell struct { | ||
| 35 | + ID string `xml:"id,attr"` | ||
| 36 | + Value string `xml:"value,attr"` | ||
| 37 | + Edge string `xml:"edge,attr"` | ||
| 38 | + Source string `xml:"source,attr"` | ||
| 39 | + Target string `xml:"target,attr"` | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +// boldLabel is the package name inside a box: drawio stores the label as | ||
| 43 | +// escaped HTML, and the name is the part in bold. | ||
| 44 | +var boldLabel = regexp.MustCompile(`(?s)<b>(.*?)</b>`) | ||
| 45 | + | ||
| 46 | +// readDiagram parses the diagram, failing the test rather than returning an | ||
| 47 | +// error — a diagram that will not parse is not a case any caller can handle. | ||
| 48 | +func readDiagram(t *testing.T) mxFile { | ||
| 49 | + t.Helper() | ||
| 50 | + | ||
| 51 | + raw, err := os.ReadFile(diagramFile) | ||
| 52 | + if err != nil { | ||
| 53 | + t.Fatalf("reading %s: %v", diagramFile, err) | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + var file mxFile | ||
| 57 | + if err := xml.Unmarshal(raw, &file); err != nil { | ||
| 58 | + t.Fatalf("parsing %s: %v", diagramFile, err) | ||
| 59 | + } | ||
| 60 | + return file | ||
| 61 | +} | ||
| 62 | + | ||
| 63 | +// boxes maps each box's package name to the id the arrows use for it. | ||
| 64 | +func boxes(t *testing.T, file mxFile) map[string]string { | ||
| 65 | + t.Helper() | ||
| 66 | + | ||
| 67 | + found := map[string]string{} | ||
| 68 | + for _, cell := range file.Cells { | ||
| 69 | + if cell.Edge == "1" || cell.Value == "" { | ||
| 70 | + continue | ||
| 71 | + } | ||
| 72 | + label := html.UnescapeString(cell.Value) | ||
| 73 | + // A box's package name is the part in bold, where there is one; the | ||
| 74 | + // third-party box carries its name plain, with nothing to tell apart | ||
| 75 | + // from it. | ||
| 76 | + if match := boldLabel.FindStringSubmatch(label); match != nil { | ||
| 77 | + label = match[1] | ||
| 78 | + } | ||
| 79 | + found[label] = cell.ID | ||
| 80 | + } | ||
| 81 | + return found | ||
| 82 | +} | ||
| 83 | + | ||
| 84 | +// imports asks the toolchain what a package imports, shortened to the names the | ||
| 85 | +// diagram uses: the last element for a turbo-core package, the module-relative | ||
| 86 | +// path for one of ours, and "tcell/v2" for the one third-party dependency. | ||
| 87 | +func imports(t *testing.T, pkg string) []string { | ||
| 88 | + t.Helper() | ||
| 89 | + | ||
| 90 | + out, err := exec.Command("go", "list", "-f", `{{join .Imports "\n"}}`, pkg).Output() | ||
| 91 | + if err != nil { | ||
| 92 | + t.Fatalf("go list %s: %v", pkg, err) | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + var names []string | ||
| 96 | + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { | ||
| 97 | + switch { | ||
| 98 | + case strings.HasPrefix(line, "rickub.com/turbo-editors/turbo-core/"): | ||
| 99 | + names = append(names, strings.TrimPrefix(line, "rickub.com/turbo-editors/turbo-core/")) | ||
| 100 | + case strings.HasPrefix(line, "rickub.com/turbo-editors/turbo-golo/"): | ||
| 101 | + names = append(names, strings.TrimPrefix(line, "rickub.com/turbo-editors/turbo-golo/")) | ||
| 102 | + case strings.HasPrefix(line, "github.com/gdamore/tcell/"): | ||
| 103 | + names = append(names, "tcell/v2") | ||
| 104 | + } | ||
| 105 | + } | ||
| 106 | + sort.Strings(names) | ||
| 107 | + return names | ||
| 108 | +} | ||
| 109 | + | ||
| 110 | +// The boxes are exactly the packages the two packages of this module import, | ||
| 111 | +// plus the two packages themselves. A box for a package nothing imports is as | ||
| 112 | +// wrong as a missing one: both tell a reader something untrue about the code. | ||
| 113 | +func TestTheDiagramDrawsExactlyThePackagesThisModuleImports(t *testing.T) { | ||
| 114 | + drawn := boxes(t, readDiagram(t)) | ||
| 115 | + | ||
| 116 | + want := map[string]bool{"main": true, "internal/gololang": true} | ||
| 117 | + for _, pkg := range append(imports(t, "."), imports(t, "./internal/gololang")...) { | ||
| 118 | + want[pkg] = true | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + for name := range want { | ||
| 122 | + if _, ok := drawn[name]; !ok { | ||
| 123 | + t.Errorf("%s draws no box for %q", diagramFile, name) | ||
| 124 | + } | ||
| 125 | + } | ||
| 126 | + for name := range drawn { | ||
| 127 | + if !want[name] { | ||
| 128 | + t.Errorf("%s draws a box for %q, which nothing in this module imports", diagramFile, name) | ||
| 129 | + } | ||
| 130 | + } | ||
| 131 | +} | ||
| 132 | + | ||
| 133 | +// Every arrow leaving one of our two boxes is an import that exists. This is | ||
| 134 | +// the half that caught the copied diagram: an arrow drawn out of a box labelled | ||
| 135 | +// internal/rustlang cannot be checked at all until the box is named right. | ||
| 136 | +func TestEveryArrowOutOfOurPackagesIsARealImport(t *testing.T) { | ||
| 137 | + file := readDiagram(t) | ||
| 138 | + drawn := boxes(t, file) | ||
| 139 | + | ||
| 140 | + byID := map[string]string{} | ||
| 141 | + for name, id := range drawn { | ||
| 142 | + byID[id] = name | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + ours := map[string]string{"main": ".", "internal/gololang": "./internal/gololang"} | ||
| 146 | + for _, cell := range file.Cells { | ||
| 147 | + if cell.Edge != "1" { | ||
| 148 | + continue | ||
| 149 | + } | ||
| 150 | + from, ok := byID[cell.Source] | ||
| 151 | + if !ok { | ||
| 152 | + t.Errorf("%s draws an arrow out of unknown cell %q", diagramFile, cell.Source) | ||
| 153 | + continue | ||
| 154 | + } | ||
| 155 | + pkg, ok := ours[from] | ||
| 156 | + if !ok { | ||
| 157 | + continue | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + to := byID[cell.Target] | ||
| 161 | + if to == "internal/gololang" && from == "main" { | ||
| 162 | + continue // main imports it under its full path, already shortened | ||
| 163 | + } | ||
| 164 | + if !slicesContain(imports(t, pkg), to) { | ||
| 165 | + t.Errorf("%s draws %s → %s, but %s imports no such package", diagramFile, from, to, from) | ||
| 166 | + } | ||
| 167 | + } | ||
| 168 | +} | ||
| 169 | + | ||
| 170 | +// The file's host attribute names the project it was drawn for. It is the one | ||
| 171 | +// field a reader never sees and a copy always keeps. | ||
| 172 | +func TestTheDiagramSaysWhichProjectItWasDrawnFor(t *testing.T) { | ||
| 173 | + if host := readDiagram(t).Host; host != "turbo-golo" { | ||
| 174 | + t.Errorf("%s was drawn for %q, not turbo-golo", diagramFile, host) | ||
| 175 | + } | ||
| 176 | +} | ||
| 177 | + | ||
| 178 | +// No label anywhere in the diagram names another editor in the family, or the | ||
| 179 | +// language it edits. The copied diagram said "the Rust scanner" in prose that | ||
| 180 | +// no identifier check would have looked at. | ||
| 181 | +func TestNoLabelInTheDiagramNamesAnotherEditorsLanguage(t *testing.T) { | ||
| 182 | + for _, cell := range readDiagram(t).Cells { | ||
| 183 | + label := html.UnescapeString(cell.Value) | ||
| 184 | + for _, other := range []string{"moonbitlang", "pythonlang", "rustlang", "golang", "MoonBit", "Python", "Rust", "Go ", "turbo-moonbit", "turbo-python", "turbo-rust", "turbo-go"} { | ||
| 185 | + if strings.Contains(label, other) { | ||
| 186 | + t.Errorf("%s labels a cell %q, which names %q", diagramFile, label, other) | ||
| 187 | + } | ||
| 188 | + } | ||
| 189 | + } | ||
| 190 | +} | ||
| 191 | + | ||
| 192 | +// slicesContain says whether a sorted list holds a value. It is here rather | ||
| 193 | +// than from the standard library's slices package so the test reads the same | ||
| 194 | +// way in a checkout of any Go version this module supports. | ||
| 195 | +func slicesContain(list []string, want string) bool { | ||
| 196 | + for _, got := range list { | ||
| 197 | + if got == want { | ||
| 198 | + return true | ||
| 199 | + } | ||
| 200 | + } | ||
| 201 | + return false | ||
| 202 | +} | ||
added
docs/README.md +10 -0 | new file mode 100644 | ||
| @@ -0,0 +1,10 @@ | ||
| 1 | +# Turbo Golo — documentation | |
| 2 | + | |
| 3 | +Choose your language / Choisissez votre langue : | |
| 4 | + | |
| 5 | +- 🇬🇧 **[English](en/)** | |
| 6 | +- 🇫🇷 **[Français](fr/)** | |
| 7 | + | |
| 8 | +Both are complete and organised the same way, following the [Diátaxis](https://diataxis.fr) method: tutorials, how-to guides, reference, explanation. | |
| 9 | + | |
| 10 | +The [package dependency diagram](diagrams/packages.drawio) is language-neutral and shared by both. | |
| new file mode 100644 | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | +# Turbo Golo — documentation | ||
| 2 | + | ||
| 3 | +Choose your language / Choisissez votre langue : | ||
| 4 | + | ||
| 5 | +- 🇬🇧 **[English](en/)** | ||
| 6 | +- 🇫🇷 **[Français](fr/)** | ||
| 7 | + | ||
| 8 | +Both are complete and organised the same way, following the [Diátaxis](https://diataxis.fr) method: tutorials, how-to guides, reference, explanation. | ||
| 9 | + | ||
| 10 | +The [package dependency diagram](diagrams/packages.drawio) is language-neutral and shared by both. | ||
added
docs/diagrams/packages.drawio +28 -0 | new file mode 100644 | ||
| @@ -0,0 +1,28 @@ | ||
| 1 | +<mxfile host="turbo-golo" modified="" agent="generated from go list -deps"> | |
| 2 | + <diagram name="packages" id="packages"> | |
| 3 | + <mxGraphModel dx="1400" dy="900" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0"> | |
| 4 | + <root> | |
| 5 | + <mxCell id="0"/> | |
| 6 | + <mxCell id="1" parent="0"/> | |
| 7 | + <mxCell id="2" value="<b>main</b><br/><font style='font-size:10px'>flags, terminal, wiring</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1"><mxGeometry x="-95" y="0" width="190" height="54" as="geometry"/></mxCell> | |
| 8 | + <mxCell id="3" value="<b>internal/gololang</b><br/><font style='font-size:10px'>the profile and the Golo scanner</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1"><mxGeometry x="-95" y="130" width="190" height="54" as="geometry"/></mxCell> | |
| 9 | + <mxCell id="4" value="<b>app</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-519" y="260" width="190" height="54" as="geometry"/></mxCell> | |
| 10 | + <mxCell id="5" value="<b>profile</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-307" y="260" width="190" height="54" as="geometry"/></mxCell> | |
| 11 | + <mxCell id="6" value="<b>settings</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="260" width="190" height="54" as="geometry"/></mxCell> | |
| 12 | + <mxCell id="7" value="<b>syntax</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="117" y="260" width="190" height="54" as="geometry"/></mxCell> | |
| 13 | + <mxCell id="8" value="<b>theme</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="329" y="260" width="190" height="54" as="geometry"/></mxCell> | |
| 14 | + <mxCell id="9" value="<b>version</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="390" width="190" height="54" as="geometry"/></mxCell> | |
| 15 | + <mxCell id="10" value="tcell/v2" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#999999;dashed=1;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="520" width="190" height="54" as="geometry"/></mxCell> | |
| 16 | + <mxCell id="e11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="3" target="5"><mxGeometry relative="1" as="geometry"/></mxCell> | |
| 17 | + <mxCell id="e12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="3" target="7"><mxGeometry relative="1" as="geometry"/></mxCell> | |
| 18 | + <mxCell id="e13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="3"><mxGeometry relative="1" as="geometry"/></mxCell> | |
| 19 | + <mxCell id="e14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="10"><mxGeometry relative="1" as="geometry"/></mxCell> | |
| 20 | + <mxCell id="e15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="4"><mxGeometry relative="1" as="geometry"/></mxCell> | |
| 21 | + <mxCell id="e16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="5"><mxGeometry relative="1" as="geometry"/></mxCell> | |
| 22 | + <mxCell id="e17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="6"><mxGeometry relative="1" as="geometry"/></mxCell> | |
| 23 | + <mxCell id="e18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="8"><mxGeometry relative="1" as="geometry"/></mxCell> | |
| 24 | + <mxCell id="e19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="9"><mxGeometry relative="1" as="geometry"/></mxCell> | |
| 25 | + </root> | |
| 26 | + </mxGraphModel> | |
| 27 | + </diagram> | |
| 28 | +</mxfile> | |
| new file mode 100644 | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | +<mxfile host="turbo-golo" modified="" agent="generated from go list -deps"> | ||
| 2 | + <diagram name="packages" id="packages"> | ||
| 3 | + <mxGraphModel dx="1400" dy="900" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0"> | ||
| 4 | + <root> | ||
| 5 | + <mxCell id="0"/> | ||
| 6 | + <mxCell id="1" parent="0"/> | ||
| 7 | + <mxCell id="2" value="<b>main</b><br/><font style='font-size:10px'>flags, terminal, wiring</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1"><mxGeometry x="-95" y="0" width="190" height="54" as="geometry"/></mxCell> | ||
| 8 | + <mxCell id="3" value="<b>internal/gololang</b><br/><font style='font-size:10px'>the profile and the Golo scanner</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1"><mxGeometry x="-95" y="130" width="190" height="54" as="geometry"/></mxCell> | ||
| 9 | + <mxCell id="4" value="<b>app</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-519" y="260" width="190" height="54" as="geometry"/></mxCell> | ||
| 10 | + <mxCell id="5" value="<b>profile</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-307" y="260" width="190" height="54" as="geometry"/></mxCell> | ||
| 11 | + <mxCell id="6" value="<b>settings</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="260" width="190" height="54" as="geometry"/></mxCell> | ||
| 12 | + <mxCell id="7" value="<b>syntax</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="117" y="260" width="190" height="54" as="geometry"/></mxCell> | ||
| 13 | + <mxCell id="8" value="<b>theme</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="329" y="260" width="190" height="54" as="geometry"/></mxCell> | ||
| 14 | + <mxCell id="9" value="<b>version</b><br/><font style='font-size:9px'>turbo-core</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="390" width="190" height="54" as="geometry"/></mxCell> | ||
| 15 | + <mxCell id="10" value="tcell/v2" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#999999;dashed=1;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="520" width="190" height="54" as="geometry"/></mxCell> | ||
| 16 | + <mxCell id="e11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="3" target="5"><mxGeometry relative="1" as="geometry"/></mxCell> | ||
| 17 | + <mxCell id="e12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="3" target="7"><mxGeometry relative="1" as="geometry"/></mxCell> | ||
| 18 | + <mxCell id="e13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="3"><mxGeometry relative="1" as="geometry"/></mxCell> | ||
| 19 | + <mxCell id="e14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="10"><mxGeometry relative="1" as="geometry"/></mxCell> | ||
| 20 | + <mxCell id="e15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="4"><mxGeometry relative="1" as="geometry"/></mxCell> | ||
| 21 | + <mxCell id="e16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="5"><mxGeometry relative="1" as="geometry"/></mxCell> | ||
| 22 | + <mxCell id="e17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="6"><mxGeometry relative="1" as="geometry"/></mxCell> | ||
| 23 | + <mxCell id="e18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="8"><mxGeometry relative="1" as="geometry"/></mxCell> | ||
| 24 | + <mxCell id="e19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="9"><mxGeometry relative="1" as="geometry"/></mxCell> | ||
| 25 | + </root> | ||
| 26 | + </mxGraphModel> | ||
| 27 | + </diagram> | ||
| 28 | +</mxfile> | ||
added
docs/en/README.md +61 -0 | new file mode 100644 | ||
| @@ -0,0 +1,61 @@ | ||
| 1 | +# Turbo Golo — documentation | |
| 2 | + | |
| 3 | +Turbo Golo is a Turbo C-style editor for Golo: a full-screen terminal IDE with menus, movable windows, syntax colouring for nine languages, themes, completion from `golo lsp`, shell windows, per-project settings, a project tree, snippets, and the Golo toolchain a menu away. | |
| 4 | + | |
| 5 | +This documentation follows the [Diátaxis](https://diataxis.fr) method. Four kinds of page, four different needs — go to the one that matches what you want right now. | |
| 6 | + | |
| 7 | +| I want to… | Go to | | |
| 8 | +| --- | --- | | |
| 9 | +| **learn** the editor by using it | [Tutorials](tutorials/) | | |
| 10 | +| **do** something specific | [How-to guides](how-to/) | | |
| 11 | +| **look up** an exact detail | [Reference](reference/) | | |
| 12 | +| **understand** how and why it works | [Explanation](explanation/) | | |
| 13 | + | |
| 14 | +## Tutorials — learning by doing | |
| 15 | + | |
| 16 | +- [Your first Golo program in Turbo Golo](tutorials/getting-started.md) — install it, write a script, run it, break it and see the editor say where. | |
| 17 | +- [Demo projects](../../demos/) — three Golo programs to open in the editor once you have it: the smallest one, one with structs, a union and a test file beside it, and a tour of every construct the scanner colours. | |
| 18 | + | |
| 19 | +## How-to guides — recipes for a task | |
| 20 | + | |
| 21 | +- [How to install and build Turbo Golo](how-to/install.md) | |
| 22 | +- [How to install GoloScript](how-to/install-goloscript.md) | |
| 23 | +- [How to run the tests](how-to/run-the-tests.md) | |
| 24 | +- [How to enable Golo completion](how-to/enable-completion.md) | |
| 25 | +- [How to write your own theme](how-to/write-a-theme.md) | |
| 26 | +- [How to move around a file](how-to/navigate-code.md) | |
| 27 | +- [How to ask what the code means](how-to/ask-about-code.md) | |
| 28 | +- [How to run shell commands without leaving the editor](how-to/use-a-terminal.md) | |
| 29 | +- [How to give a project its own settings](how-to/configure-a-project.md) | |
| 30 | +- [How to browse a project and open files from a tree](how-to/browse-a-project.md) | |
| 31 | +- [How to insert snippets from a menu](how-to/use-snippets.md) | |
| 32 | +- [How to run Golo commands from the editor](how-to/run-golo-commands.md) | |
| 33 | +- [How to make a release](how-to/make-a-release.md) | |
| 34 | +- [How to talk to a coding agent from the editor](how-to/talk-to-an-agent.md) | |
| 35 | + | |
| 36 | +## Reference — the exact details | |
| 37 | + | |
| 38 | +- [Command line](reference/cli.md) | |
| 39 | +- [Keyboard](reference/keyboard.md) | |
| 40 | +- [Menus](reference/menus.md) | |
| 41 | +- [Theme file format](reference/themes.md) | |
| 42 | +- [Terminal windows](reference/terminal.md) | |
| 43 | +- [Project settings](reference/project-settings.md) | |
| 44 | +- [Project tree](reference/project-tree.md) | |
| 45 | +- [Languages coloured](reference/languages.md) | |
| 46 | +- [Snippets](reference/snippets.md) | |
| 47 | +- [Golo tools](reference/golo-tools.md) | |
| 48 | +- [The version number](reference/versioning.md) | |
| 49 | +- [Agents and ACP](reference/acp.md) | |
| 50 | + | |
| 51 | +## Explanation — understanding | |
| 52 | + | |
| 53 | +- [Architecture](explanation/architecture.md) | |
| 54 | +- [Design decisions](explanation/design-decisions.md) | |
| 55 | +- [Colouring and completion](explanation/colouring-and-completion.md) | |
| 56 | +- [Terminal windows](explanation/terminal-windows.md) | |
| 57 | +- [Project settings](explanation/project-settings.md) | |
| 58 | +- [Project tree](explanation/project-tree.md) | |
| 59 | +- [Snippets](explanation/snippets.md) | |
| 60 | +- [Golo tools](explanation/golo-tools.md) | |
| 61 | +- [Agent windows](explanation/agent-windows.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,61 @@ | |||
| 1 | +# Turbo Golo — documentation | ||
| 2 | + | ||
| 3 | +Turbo Golo is a Turbo C-style editor for Golo: a full-screen terminal IDE with menus, movable windows, syntax colouring for nine languages, themes, completion from `golo lsp`, shell windows, per-project settings, a project tree, snippets, and the Golo toolchain a menu away. | ||
| 4 | + | ||
| 5 | +This documentation follows the [Diátaxis](https://diataxis.fr) method. Four kinds of page, four different needs — go to the one that matches what you want right now. | ||
| 6 | + | ||
| 7 | +| I want to… | Go to | | ||
| 8 | +| --- | --- | | ||
| 9 | +| **learn** the editor by using it | [Tutorials](tutorials/) | | ||
| 10 | +| **do** something specific | [How-to guides](how-to/) | | ||
| 11 | +| **look up** an exact detail | [Reference](reference/) | | ||
| 12 | +| **understand** how and why it works | [Explanation](explanation/) | | ||
| 13 | + | ||
| 14 | +## Tutorials — learning by doing | ||
| 15 | + | ||
| 16 | +- [Your first Golo program in Turbo Golo](tutorials/getting-started.md) — install it, write a script, run it, break it and see the editor say where. | ||
| 17 | +- [Demo projects](../../demos/) — three Golo programs to open in the editor once you have it: the smallest one, one with structs, a union and a test file beside it, and a tour of every construct the scanner colours. | ||
| 18 | + | ||
| 19 | +## How-to guides — recipes for a task | ||
| 20 | + | ||
| 21 | +- [How to install and build Turbo Golo](how-to/install.md) | ||
| 22 | +- [How to install GoloScript](how-to/install-goloscript.md) | ||
| 23 | +- [How to run the tests](how-to/run-the-tests.md) | ||
| 24 | +- [How to enable Golo completion](how-to/enable-completion.md) | ||
| 25 | +- [How to write your own theme](how-to/write-a-theme.md) | ||
| 26 | +- [How to move around a file](how-to/navigate-code.md) | ||
| 27 | +- [How to ask what the code means](how-to/ask-about-code.md) | ||
| 28 | +- [How to run shell commands without leaving the editor](how-to/use-a-terminal.md) | ||
| 29 | +- [How to give a project its own settings](how-to/configure-a-project.md) | ||
| 30 | +- [How to browse a project and open files from a tree](how-to/browse-a-project.md) | ||
| 31 | +- [How to insert snippets from a menu](how-to/use-snippets.md) | ||
| 32 | +- [How to run Golo commands from the editor](how-to/run-golo-commands.md) | ||
| 33 | +- [How to make a release](how-to/make-a-release.md) | ||
| 34 | +- [How to talk to a coding agent from the editor](how-to/talk-to-an-agent.md) | ||
| 35 | + | ||
| 36 | +## Reference — the exact details | ||
| 37 | + | ||
| 38 | +- [Command line](reference/cli.md) | ||
| 39 | +- [Keyboard](reference/keyboard.md) | ||
| 40 | +- [Menus](reference/menus.md) | ||
| 41 | +- [Theme file format](reference/themes.md) | ||
| 42 | +- [Terminal windows](reference/terminal.md) | ||
| 43 | +- [Project settings](reference/project-settings.md) | ||
| 44 | +- [Project tree](reference/project-tree.md) | ||
| 45 | +- [Languages coloured](reference/languages.md) | ||
| 46 | +- [Snippets](reference/snippets.md) | ||
| 47 | +- [Golo tools](reference/golo-tools.md) | ||
| 48 | +- [The version number](reference/versioning.md) | ||
| 49 | +- [Agents and ACP](reference/acp.md) | ||
| 50 | + | ||
| 51 | +## Explanation — understanding | ||
| 52 | + | ||
| 53 | +- [Architecture](explanation/architecture.md) | ||
| 54 | +- [Design decisions](explanation/design-decisions.md) | ||
| 55 | +- [Colouring and completion](explanation/colouring-and-completion.md) | ||
| 56 | +- [Terminal windows](explanation/terminal-windows.md) | ||
| 57 | +- [Project settings](explanation/project-settings.md) | ||
| 58 | +- [Project tree](explanation/project-tree.md) | ||
| 59 | +- [Snippets](explanation/snippets.md) | ||
| 60 | +- [Golo tools](explanation/golo-tools.md) | ||
| 61 | +- [Agent windows](explanation/agent-windows.md) | ||
added
docs/en/explanation/agent-windows.md +114 -0 | new file mode 100644 | ||
| @@ -0,0 +1,114 @@ | ||
| 1 | +# Agent windows | |
| 2 | + | |
| 3 | +This page is about why talking to an agent is shaped the way it is. For how to do it, see [How to talk to a coding agent](../how-to/talk-to-an-agent.md); for the exact keys and file format, [Agents and ACP](../reference/acp.md). | |
| 4 | + | |
| 5 | +## Why a protocol rather than a provider | |
| 6 | + | |
| 7 | +An editor that wanted to offer a chat window had two ways to get one. It could speak to model providers directly — an HTTP client per provider, a set of API keys to store, a tool-calling loop to write, and a new one of each every time somebody wants a provider the editor has never heard of. Or it could speak one protocol to whatever program the user already trusts to do that work. | |
| 8 | + | |
| 9 | +The [Agent Client Protocol](https://agentclientprotocol.com) is the second. The agent is a child process; the editor sends it prompts and draws what comes back. The editor holds no API key, knows no provider, and implements no tool-calling loop — and the same code talks to `docker agent` against a local llama.cpp, to a cloud agent, or to something you wrote this afternoon. | |
| 10 | + | |
| 11 | +It also means the editor is not the place a new model lands. Support for one is a line in *your* agent's configuration file, which is a file this editor does not read. | |
| 12 | + | |
| 13 | +## Why this lives in turbo-core | |
| 14 | + | |
| 15 | +Turbo Golo is [a command, a profile and a scanner](architecture.md); everything else is the library every Turbo editor shares. An agent window is a window, a menu, a modal dialog and a turn of the event loop — all four of which belong to `turbo-core/app`. Building it here would have meant adding a general "let an editor add a window and a menu from outside" seam to the library and then using it exactly once. | |
| 16 | + | |
| 17 | +So the protocol client, the conversation model and the window are `turbo-core/acp`, beside `terminal` and `filetree`, which are the same shape. What Turbo Golo contributes is the starter `acp.toml` it offers to write — the one part of this that is about Golo projects. Turbo Rust and Turbo Python get agent windows by writing a starter file of their own, and nothing else. | |
| 18 | + | |
| 19 | +## Why a window, not a panel | |
| 20 | + | |
| 21 | +The same reasoning the [project tree](project-tree.md) settled. A docked panel would mean the desktop growing a notion of reserved edges, and `fitInto`, the grow modes, maximising, tiling and cascading all having to respect them — a change to the foundation of the interface for one widget. As an ordinary window an agent gets `F6`, `Alt`-digits, `[x]`, `[■]` and Tile for free. | |
| 22 | + | |
| 23 | +It also makes "several agents at once" fall out rather than being designed: two windows are two processes and two conversations, and Tile puts a fast local model beside a careful slow one. A panel would have had to grow tabs to do that. | |
| 24 | + | |
| 25 | +## Why one process per window, started when the window opens | |
| 26 | + | |
| 27 | +An agent is a conversation, and a conversation has a beginning. Starting the process with the window means the agent's working directory, its environment and its session all belong to that window, and closing it is an unambiguous end — the same bargain [terminal windows](terminal-windows.md) make, and for the same reason: what the window holds is a running process, not unsaved work, so closing it asks nothing. | |
| 28 | + | |
| 29 | +The alternative — one long-lived agent multiplexed across several windows — would have meant the editor deciding which window a `session/update` belonged to, and what to do with a window whose session had gone away while the process lived on. Two processes are cheaper than that bookkeeping. | |
| 30 | + | |
| 31 | +## Why the permission dialog is opened from the event loop, not from the message | |
| 32 | + | |
| 33 | +`session/request_permission` arrives on the connection's reading goroutine, and the answer comes from a dialog the user has to look at. The reply therefore cannot be made where the request is handled, and the dialog cannot be opened there either: everything that draws belongs to the main goroutine. | |
| 34 | + | |
| 35 | +So the request is *recorded*, and the event loop notices it on its next turn and opens the dialog. This is the fourth time this project has reached the same conclusion — [autosave](project-settings.md), the language server's re-announcement, and the terminal's redraws are the others — and the reason is always the same: `PostEvent` is allowed to drop what does not fit, so an event may cause a turn of the loop but must never be the only thing that carries a fact. | |
| 36 | + | |
| 37 | +That is why the JSON-RPC layer had to learn to answer a request *later*. It is also the whole of why `jsonrpc` was extracted out of `lsp`: a language server's questions can all be answered on the spot, and an agent's cannot. | |
| 38 | + | |
| 39 | +## Why the agent is offered the buffer rather than the file | |
| 40 | + | |
| 41 | +When the agent reads a file you have open and have not saved, it is given the text you can see, not the text on disk. The alternative is an agent that reviews the version you have just moved past, which is wrong precisely when you are most likely to be asking — you changed something and want to know about the change. | |
| 42 | + | |
| 43 | +The cost is that the agent sees text that no other tool can see, so an answer quoting a line number may not match what `gogolo build` says. That is accepted: the same is already true of completion, which has answered from the buffer since the editor learnt to talk to `golo`. | |
| 44 | + | |
| 45 | +Writes go the same way, into the buffer, marked modified. An agent that edits a file leaves the change in front of you, undoable with `Ctrl-Z` and unsaved until you press `F2`. An agent quietly rewriting a file under a window you have open would be the worst possible version of this feature. | |
| 46 | + | |
| 47 | +## Why the colours are the syntax classes, and not new theme keys | |
| 48 | + | |
| 49 | +The [project tree](project-tree.md) needed theme keys of its own, because it would otherwise have borrowed `list.selected`, a colour chosen against a *dialog* background, and drawn its selected row in the colour underneath it. Nothing like that is true here: an agent window's body is `window.body`, which is what the syntax classes are already chosen against and already tested against for contrast. | |
| 50 | + | |
| 51 | +So a speaker's name is drawn in the keyword style, a thought in the comment style, a tool call in the type style, and code in whatever its own scanner says. Eleven themes therefore colour agent windows correctly without being touched, and a theme somebody wrote last year does too. | |
| 52 | + | |
| 53 | +What is given up is expressiveness: a theme cannot make thoughts quiet without also making comments quiet, because they are the same key. If that turns out to matter in use, `agent.*` keys can be added later — the contrast rules and the completeness test are the cost, and they are worth paying only if somebody wants the distinction. | |
| 54 | + | |
| 55 | +## Why the transcript is a model the window merely draws | |
| 56 | + | |
| 57 | +The agent sends tokens: `"I"`, `" found"`, `" agent"`, `".yaml"`. A window that appended each one to a list of lines would be a window that could not reflow, could not tell prose from a fenced code block, and could not be tested without a live agent. | |
| 58 | + | |
| 59 | +So the conversation is a value — `acp.Transcript` — that coalesces chunks into entries, folds each `tool_call_update` onto the `tool_call` its id matches, and hands the window a list of blocks that are either prose or code-in-a-named-language. It knows nothing about a terminal, which is what lets it be tested by calling functions and comparing values, the same organising rule `buffer`, `lsp` and `syntax` follow. | |
| 60 | + | |
| 61 | +It is also what makes the drawing tests deterministic. The project has been bitten before by tests that asserted on a screen while a live process wrote to it, and that hid a real fault for a whole session; a window drawn from a fixed transcript cannot race anything. | |
| 62 | + | |
| 63 | +## What was deliberately left out | |
| 64 | + | |
| 65 | +- **Session resume.** `session/load` exists, and using it would mean deciding where conversations are stored, how long they are kept, and what happens when the project moved. That is a feature in its own right. | |
| 66 | +- **Authentication.** An agent that needs a login is told to log in with its own CLI. Storing a credential is a responsibility this editor has so far avoided entirely, and one protocol method is not a good reason to start. | |
| 67 | +- **The terminal capability.** An agent can already have a shell through its own toolsets, as `docker agent` does. Advertising `terminal` would mean the editor running commands on the agent's behalf and owning the output — the tools menu already does that, better, for commands *you* chose. | |
| 68 | +- **Images in prompts.** The editor has text and files to send, and a terminal to draw in. | |
| 69 | + | |
| 70 | +## See also | |
| 71 | + | |
| 72 | +- [Architecture](architecture.md) — what is here and what is in the library | |
| 73 | +- [Terminal windows](terminal-windows.md) — the other window holding a live process | |
| 74 | +- [Project tree](project-tree.md) — where the window-not-a-panel argument was first made | |
| 75 | + | |
| 76 | +## Why copying goes to two clipboards | |
| 77 | + | |
| 78 | +"Copy this so I can use it elsewhere" usually means *elsewhere entirely* — another window, a browser, a message to a colleague. A clipboard that only worked inside this editor would answer the smaller half of the request, and the half you were least likely to be asking about. | |
| 79 | + | |
| 80 | +So a copy goes to both: the editor's own, which `Shift-Ins` pastes from, and the system's, reached by asking the terminal through OSC 52. Nothing verifies the second, because there is nothing to verify — the sequence has no reply, a terminal may refuse it for security, and some need it turned on. A message promising something that did not happen would be worse than one that stays quiet, so the status bar says only how many lines were copied, which is true either way. | |
| 81 | + | |
| 82 | +## Why copying with nothing selected copies a whole block | |
| 83 | + | |
| 84 | +The thing somebody wants out of a conversation is almost always a code block. Making them select it first — six keystrokes, or a drag they have to aim — is work the editor already has the information to do for them: it laid the conversation out, so it knows exactly where that block starts and ends. | |
| 85 | + | |
| 86 | +So the lines carry a **region**: one fenced code block, one passage of prose, one tool call's output. With nothing selected, `Ctrl-C` copies the region the cursor is on. A speaker's label and a tool call's heading are furniture and get regions of their own, which is what keeps `‣ Bob (llama.cpp)` out of a block pasted into a source file. | |
| 87 | + | |
| 88 | +That last part was not designed; it was found. The first version copied the label along with the code, and it was caught by copying from the real binary and reading the OSC 52 payload back off the wire. | |
| 89 | + | |
| 90 | +## Why the spinner is drawn from the clock | |
| 91 | + | |
| 92 | +An agent thinking for twenty seconds sends nothing at all, and a window that looked frozen would be indistinguishable from one that was. The spinner is the cheapest possible answer to "is this still working?". | |
| 93 | + | |
| 94 | +It is a function of the time — `Spinner(now)` — rather than a counter something increments. Nothing has to be reset when a turn begins, two windows thinking at once turn in step, and a test can assert on a frame without waiting for one, which is the same reason `editor.View` and `app.App` both take an injectable clock. | |
| 95 | + | |
| 96 | +Drawing from the clock means something else has to *cause* the redraw, so a session running a turn wakes the event loop at the spinner's own rate. That is allowed to be a ticker precisely because a dropped tick cannot strand anything: it asks for a turn of the loop and never carries a fact — the rule this project has now reached five times. | |
| 97 | + | |
| 98 | +The window's **title** deliberately does not animate. It is also what the window list and the `Alt`-digit menu show, and a name that changed eight times a second would make both of them flicker for no gain. | |
| 99 | + | |
| 100 | +## Why commands are a popup in the box, and not a menu | |
| 101 | + | |
| 102 | +An agent's commands arrive over the wire as a list — `available_commands_update` — and may change during the session. A menu built from them would have to be rebuilt on every update, would sit far from where the command is typed, and would still have to end by putting `/web ` into the box, because that is the only thing the protocol lets a client send: a command is a text prompt the agent recognises by its first word. | |
| 103 | + | |
| 104 | +So the list opens where the text is, on the character that starts a command, and closes when the word is complete. It is the same shape as the completion popup over a file, for the same reason: what you are choosing is what you are typing. Using `/` and `@` rather than keys of the editor's own is deliberate — they are the characters Zed uses, so an agent's own documentation is true here without a translation table. | |
| 105 | + | |
| 106 | +`Enter` has two meanings on the list, ordered by how finished the word is: it completes an unfinished one, and sends a finished one. The alternative — `Enter` always completes, a second `Enter` sends — costs a keystroke on every command and gains nothing, because a word that already reads exactly as a command has nothing left to complete. | |
| 107 | + | |
| 108 | +## Why a mention carries the file, when it can | |
| 109 | + | |
| 110 | +The protocol offers two ways to name a file in a prompt: a `resource_link`, which is a URI the agent fetches for itself, and an embedded `resource`, which is the URI *and the text*. The specification calls the second "the preferred way to include context", and the reason is the same one that makes `fs/read_text_file` answer from the buffer: the editor knows things about the file that the disk does not. An agent following a link to a file you have edited and not saved reads the version you have just moved past, which is wrong precisely when you are most likely to be asking. | |
| 111 | + | |
| 112 | +So the editor sends the text when the agent declared `promptCapabilities.embeddedContext`, read through the same path `fs/read_text_file` uses, and a link otherwise — never nothing. A file that cannot be read goes as a link too, so the agent is at least told which file was meant. | |
| 113 | + | |
| 114 | +The mention replaces the name in the text rather than travelling beside it. Sending `explain @main.go` as the text *and* an attachment would give the agent the name twice and leave it to match them; putting the block where the name was gives it the file where the sentence needs it. The conversation, on the other hand, keeps the line as typed: that is what you said, and the window is a record of the conversation, not of the wire. | |
| new file mode 100644 | |||
| @@ -0,0 +1,114 @@ | |||
| 1 | +# Agent windows | ||
| 2 | + | ||
| 3 | +This page is about why talking to an agent is shaped the way it is. For how to do it, see [How to talk to a coding agent](../how-to/talk-to-an-agent.md); for the exact keys and file format, [Agents and ACP](../reference/acp.md). | ||
| 4 | + | ||
| 5 | +## Why a protocol rather than a provider | ||
| 6 | + | ||
| 7 | +An editor that wanted to offer a chat window had two ways to get one. It could speak to model providers directly — an HTTP client per provider, a set of API keys to store, a tool-calling loop to write, and a new one of each every time somebody wants a provider the editor has never heard of. Or it could speak one protocol to whatever program the user already trusts to do that work. | ||
| 8 | + | ||
| 9 | +The [Agent Client Protocol](https://agentclientprotocol.com) is the second. The agent is a child process; the editor sends it prompts and draws what comes back. The editor holds no API key, knows no provider, and implements no tool-calling loop — and the same code talks to `docker agent` against a local llama.cpp, to a cloud agent, or to something you wrote this afternoon. | ||
| 10 | + | ||
| 11 | +It also means the editor is not the place a new model lands. Support for one is a line in *your* agent's configuration file, which is a file this editor does not read. | ||
| 12 | + | ||
| 13 | +## Why this lives in turbo-core | ||
| 14 | + | ||
| 15 | +Turbo Golo is [a command, a profile and a scanner](architecture.md); everything else is the library every Turbo editor shares. An agent window is a window, a menu, a modal dialog and a turn of the event loop — all four of which belong to `turbo-core/app`. Building it here would have meant adding a general "let an editor add a window and a menu from outside" seam to the library and then using it exactly once. | ||
| 16 | + | ||
| 17 | +So the protocol client, the conversation model and the window are `turbo-core/acp`, beside `terminal` and `filetree`, which are the same shape. What Turbo Golo contributes is the starter `acp.toml` it offers to write — the one part of this that is about Golo projects. Turbo Rust and Turbo Python get agent windows by writing a starter file of their own, and nothing else. | ||
| 18 | + | ||
| 19 | +## Why a window, not a panel | ||
| 20 | + | ||
| 21 | +The same reasoning the [project tree](project-tree.md) settled. A docked panel would mean the desktop growing a notion of reserved edges, and `fitInto`, the grow modes, maximising, tiling and cascading all having to respect them — a change to the foundation of the interface for one widget. As an ordinary window an agent gets `F6`, `Alt`-digits, `[x]`, `[■]` and Tile for free. | ||
| 22 | + | ||
| 23 | +It also makes "several agents at once" fall out rather than being designed: two windows are two processes and two conversations, and Tile puts a fast local model beside a careful slow one. A panel would have had to grow tabs to do that. | ||
| 24 | + | ||
| 25 | +## Why one process per window, started when the window opens | ||
| 26 | + | ||
| 27 | +An agent is a conversation, and a conversation has a beginning. Starting the process with the window means the agent's working directory, its environment and its session all belong to that window, and closing it is an unambiguous end — the same bargain [terminal windows](terminal-windows.md) make, and for the same reason: what the window holds is a running process, not unsaved work, so closing it asks nothing. | ||
| 28 | + | ||
| 29 | +The alternative — one long-lived agent multiplexed across several windows — would have meant the editor deciding which window a `session/update` belonged to, and what to do with a window whose session had gone away while the process lived on. Two processes are cheaper than that bookkeeping. | ||
| 30 | + | ||
| 31 | +## Why the permission dialog is opened from the event loop, not from the message | ||
| 32 | + | ||
| 33 | +`session/request_permission` arrives on the connection's reading goroutine, and the answer comes from a dialog the user has to look at. The reply therefore cannot be made where the request is handled, and the dialog cannot be opened there either: everything that draws belongs to the main goroutine. | ||
| 34 | + | ||
| 35 | +So the request is *recorded*, and the event loop notices it on its next turn and opens the dialog. This is the fourth time this project has reached the same conclusion — [autosave](project-settings.md), the language server's re-announcement, and the terminal's redraws are the others — and the reason is always the same: `PostEvent` is allowed to drop what does not fit, so an event may cause a turn of the loop but must never be the only thing that carries a fact. | ||
| 36 | + | ||
| 37 | +That is why the JSON-RPC layer had to learn to answer a request *later*. It is also the whole of why `jsonrpc` was extracted out of `lsp`: a language server's questions can all be answered on the spot, and an agent's cannot. | ||
| 38 | + | ||
| 39 | +## Why the agent is offered the buffer rather than the file | ||
| 40 | + | ||
| 41 | +When the agent reads a file you have open and have not saved, it is given the text you can see, not the text on disk. The alternative is an agent that reviews the version you have just moved past, which is wrong precisely when you are most likely to be asking — you changed something and want to know about the change. | ||
| 42 | + | ||
| 43 | +The cost is that the agent sees text that no other tool can see, so an answer quoting a line number may not match what `gogolo build` says. That is accepted: the same is already true of completion, which has answered from the buffer since the editor learnt to talk to `golo`. | ||
| 44 | + | ||
| 45 | +Writes go the same way, into the buffer, marked modified. An agent that edits a file leaves the change in front of you, undoable with `Ctrl-Z` and unsaved until you press `F2`. An agent quietly rewriting a file under a window you have open would be the worst possible version of this feature. | ||
| 46 | + | ||
| 47 | +## Why the colours are the syntax classes, and not new theme keys | ||
| 48 | + | ||
| 49 | +The [project tree](project-tree.md) needed theme keys of its own, because it would otherwise have borrowed `list.selected`, a colour chosen against a *dialog* background, and drawn its selected row in the colour underneath it. Nothing like that is true here: an agent window's body is `window.body`, which is what the syntax classes are already chosen against and already tested against for contrast. | ||
| 50 | + | ||
| 51 | +So a speaker's name is drawn in the keyword style, a thought in the comment style, a tool call in the type style, and code in whatever its own scanner says. Eleven themes therefore colour agent windows correctly without being touched, and a theme somebody wrote last year does too. | ||
| 52 | + | ||
| 53 | +What is given up is expressiveness: a theme cannot make thoughts quiet without also making comments quiet, because they are the same key. If that turns out to matter in use, `agent.*` keys can be added later — the contrast rules and the completeness test are the cost, and they are worth paying only if somebody wants the distinction. | ||
| 54 | + | ||
| 55 | +## Why the transcript is a model the window merely draws | ||
| 56 | + | ||
| 57 | +The agent sends tokens: `"I"`, `" found"`, `" agent"`, `".yaml"`. A window that appended each one to a list of lines would be a window that could not reflow, could not tell prose from a fenced code block, and could not be tested without a live agent. | ||
| 58 | + | ||
| 59 | +So the conversation is a value — `acp.Transcript` — that coalesces chunks into entries, folds each `tool_call_update` onto the `tool_call` its id matches, and hands the window a list of blocks that are either prose or code-in-a-named-language. It knows nothing about a terminal, which is what lets it be tested by calling functions and comparing values, the same organising rule `buffer`, `lsp` and `syntax` follow. | ||
| 60 | + | ||
| 61 | +It is also what makes the drawing tests deterministic. The project has been bitten before by tests that asserted on a screen while a live process wrote to it, and that hid a real fault for a whole session; a window drawn from a fixed transcript cannot race anything. | ||
| 62 | + | ||
| 63 | +## What was deliberately left out | ||
| 64 | + | ||
| 65 | +- **Session resume.** `session/load` exists, and using it would mean deciding where conversations are stored, how long they are kept, and what happens when the project moved. That is a feature in its own right. | ||
| 66 | +- **Authentication.** An agent that needs a login is told to log in with its own CLI. Storing a credential is a responsibility this editor has so far avoided entirely, and one protocol method is not a good reason to start. | ||
| 67 | +- **The terminal capability.** An agent can already have a shell through its own toolsets, as `docker agent` does. Advertising `terminal` would mean the editor running commands on the agent's behalf and owning the output — the tools menu already does that, better, for commands *you* chose. | ||
| 68 | +- **Images in prompts.** The editor has text and files to send, and a terminal to draw in. | ||
| 69 | + | ||
| 70 | +## See also | ||
| 71 | + | ||
| 72 | +- [Architecture](architecture.md) — what is here and what is in the library | ||
| 73 | +- [Terminal windows](terminal-windows.md) — the other window holding a live process | ||
| 74 | +- [Project tree](project-tree.md) — where the window-not-a-panel argument was first made | ||
| 75 | + | ||
| 76 | +## Why copying goes to two clipboards | ||
| 77 | + | ||
| 78 | +"Copy this so I can use it elsewhere" usually means *elsewhere entirely* — another window, a browser, a message to a colleague. A clipboard that only worked inside this editor would answer the smaller half of the request, and the half you were least likely to be asking about. | ||
| 79 | + | ||
| 80 | +So a copy goes to both: the editor's own, which `Shift-Ins` pastes from, and the system's, reached by asking the terminal through OSC 52. Nothing verifies the second, because there is nothing to verify — the sequence has no reply, a terminal may refuse it for security, and some need it turned on. A message promising something that did not happen would be worse than one that stays quiet, so the status bar says only how many lines were copied, which is true either way. | ||
| 81 | + | ||
| 82 | +## Why copying with nothing selected copies a whole block | ||
| 83 | + | ||
| 84 | +The thing somebody wants out of a conversation is almost always a code block. Making them select it first — six keystrokes, or a drag they have to aim — is work the editor already has the information to do for them: it laid the conversation out, so it knows exactly where that block starts and ends. | ||
| 85 | + | ||
| 86 | +So the lines carry a **region**: one fenced code block, one passage of prose, one tool call's output. With nothing selected, `Ctrl-C` copies the region the cursor is on. A speaker's label and a tool call's heading are furniture and get regions of their own, which is what keeps `‣ Bob (llama.cpp)` out of a block pasted into a source file. | ||
| 87 | + | ||
| 88 | +That last part was not designed; it was found. The first version copied the label along with the code, and it was caught by copying from the real binary and reading the OSC 52 payload back off the wire. | ||
| 89 | + | ||
| 90 | +## Why the spinner is drawn from the clock | ||
| 91 | + | ||
| 92 | +An agent thinking for twenty seconds sends nothing at all, and a window that looked frozen would be indistinguishable from one that was. The spinner is the cheapest possible answer to "is this still working?". | ||
| 93 | + | ||
| 94 | +It is a function of the time — `Spinner(now)` — rather than a counter something increments. Nothing has to be reset when a turn begins, two windows thinking at once turn in step, and a test can assert on a frame without waiting for one, which is the same reason `editor.View` and `app.App` both take an injectable clock. | ||
| 95 | + | ||
| 96 | +Drawing from the clock means something else has to *cause* the redraw, so a session running a turn wakes the event loop at the spinner's own rate. That is allowed to be a ticker precisely because a dropped tick cannot strand anything: it asks for a turn of the loop and never carries a fact — the rule this project has now reached five times. | ||
| 97 | + | ||
| 98 | +The window's **title** deliberately does not animate. It is also what the window list and the `Alt`-digit menu show, and a name that changed eight times a second would make both of them flicker for no gain. | ||
| 99 | + | ||
| 100 | +## Why commands are a popup in the box, and not a menu | ||
| 101 | + | ||
| 102 | +An agent's commands arrive over the wire as a list — `available_commands_update` — and may change during the session. A menu built from them would have to be rebuilt on every update, would sit far from where the command is typed, and would still have to end by putting `/web ` into the box, because that is the only thing the protocol lets a client send: a command is a text prompt the agent recognises by its first word. | ||
| 103 | + | ||
| 104 | +So the list opens where the text is, on the character that starts a command, and closes when the word is complete. It is the same shape as the completion popup over a file, for the same reason: what you are choosing is what you are typing. Using `/` and `@` rather than keys of the editor's own is deliberate — they are the characters Zed uses, so an agent's own documentation is true here without a translation table. | ||
| 105 | + | ||
| 106 | +`Enter` has two meanings on the list, ordered by how finished the word is: it completes an unfinished one, and sends a finished one. The alternative — `Enter` always completes, a second `Enter` sends — costs a keystroke on every command and gains nothing, because a word that already reads exactly as a command has nothing left to complete. | ||
| 107 | + | ||
| 108 | +## Why a mention carries the file, when it can | ||
| 109 | + | ||
| 110 | +The protocol offers two ways to name a file in a prompt: a `resource_link`, which is a URI the agent fetches for itself, and an embedded `resource`, which is the URI *and the text*. The specification calls the second "the preferred way to include context", and the reason is the same one that makes `fs/read_text_file` answer from the buffer: the editor knows things about the file that the disk does not. An agent following a link to a file you have edited and not saved reads the version you have just moved past, which is wrong precisely when you are most likely to be asking. | ||
| 111 | + | ||
| 112 | +So the editor sends the text when the agent declared `promptCapabilities.embeddedContext`, read through the same path `fs/read_text_file` uses, and a link otherwise — never nothing. A file that cannot be read goes as a link too, so the agent is at least told which file was meant. | ||
| 113 | + | ||
| 114 | +The mention replaces the name in the text rather than travelling beside it. Sending `explain @main.go` as the text *and* an attachment would give the agent the name twice and leave it to match them; putting the block where the name was gives it the file where the sentence needs it. The conversation, on the other hand, keeps the line as typed: that is what you said, and the window is a record of the conversation, not of the wire. | ||
added
docs/en/explanation/architecture.md +111 -0 | new file mode 100644 | ||
| @@ -0,0 +1,111 @@ | ||
| 1 | +# Architecture — explanation | |
| 2 | + | |
| 3 | +## What is this about? | |
| 4 | + | |
| 5 | +Turbo Golo is a command, a profile and a scanner. Everything else — the editing widget, the windows, the menus, the dialogs, the themes, the terminal emulator, the file tree, the LSP client — is [turbo-core](https://rickub.com/turbo-editors/turbo-core), the library every Turbo editor is built on. | |
| 6 | + | |
| 7 | +This page is about that split: what is here, what is there, and why the line falls where it does. | |
| 8 | + | |
| 9 | +## What is in this repository | |
| 10 | + | |
| 11 | +``` | |
| 12 | +main.go flags, the terminal, and the wiring | |
| 13 | +internal/gololang the whole of what makes this Turbo Golo | |
| 14 | + gololang.go the profile: name, menu, server, where golo is installed | |
| 15 | + scan.go the scanner's dispatcher, comments, what crosses a line | |
| 16 | + literals.go the three quoted forms — "…", """…""" and '…' | |
| 17 | + words.go numbers, keywords, the 157 builtins, the naming conventions | |
| 18 | + templates.go three //go:embed declarations | |
| 19 | + *.toml.tmpl the three starter files a project gets, embedded | |
| 20 | +``` | |
| 21 | + | |
| 22 | +About a thousand lines counting the comments, of which some six hundred are the scanner — under five hundred lines of code by qlty's count, and a third of those are the builtin table. There is no `internal/app`, no `internal/ui`, no `internal/buffer` — those exist once, in the library, and all six editors use them unchanged. | |
| 23 | + | |
| 24 | +## What `main` does | |
| 25 | + | |
| 26 | +Six things, in this order: | |
| 27 | + | |
| 28 | +1. Parses the flags. | |
| 29 | +2. Calls `gololang.Register()`, which teaches the library to colour `.golo` files and scripts whose first line names `golo`. | |
| 30 | +3. Builds `gololang.Profile()` — the value that says this editor is Turbo Golo. | |
| 31 | +4. Reads `.turbo-golo/settings.toml` from the working directory, if there is one. | |
| 32 | +5. Opens the terminal and hands the screen, the theme name and the profile to `app.New`. | |
| 33 | +6. Starts `golo lsp` in the directory of the file being edited, and runs the event loop. | |
| 34 | + | |
| 35 | +That is the whole command. Every decision it makes — which theme wins, which files to open, whether to start a language server — is about *this run*, not about Golo. | |
| 36 | + | |
| 37 | +## The profile is the seam | |
| 38 | + | |
| 39 | +```go | |
| 40 | +profile.Profile{ | |
| 41 | + Name: "Turbo Golo", | |
| 42 | + Slug: "turbo-golo", | |
| 43 | + Language: "Golo", | |
| 44 | + ToolsMenu: "~G~olo", | |
| 45 | + RootMarkers: nil, | |
| 46 | + Server: profile.Server{Command: "golo", Args: []string{"lsp"}, …}, | |
| 47 | + Templates: profile.Templates{Settings: …, Snippets: …, Tools: …}, | |
| 48 | +} | |
| 49 | +``` | |
| 50 | + | |
| 51 | +Everything that would otherwise be a hardcoded `"turbo-golo"`, `"golo"` or `".golo"` somewhere in eleven thousand lines is one field here. The library reads them; nothing in the library knows what any of them mean. | |
| 52 | + | |
| 53 | +`Slug` carries more than it looks. The binary is `turbo-golo`, the project directory is `.turbo-golo`, the user's own configuration lives in `~/.config/turbo-golo`, and the environment variables that override it are `TURBO_GOLO_THEME_DIR` and `TURBO_GOLO_SNIPPET_DIR` — all derived from that one word. | |
| 54 | + | |
| 55 | +`RootMarkers` is the one field that is empty here and full in every sibling. Go has `go.mod`, Rust `Cargo.toml`, Python `pyproject.toml`, MoonBit `moon.mod`; Golo has no manifest at all. A script is a file and a program is a directory of them, so there is nothing to walk up towards, and the library's `ProjectRoot` — given no markers — answers with the directory of the file. That is also all `golo lsp` needs: it answers about the file it is given and resolves imports from the modules embedded in the binary, never from disk. | |
| 56 | + | |
| 57 | +## Why the scanner is here and not in the library | |
| 58 | + | |
| 59 | +turbo-core colours eight languages itself: TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell. Those are the ones every editor meets whatever it is for — a project's configuration is TOML or YAML, its documentation is Markdown, its scripts are shell, its image build a Dockerfile. | |
| 60 | + | |
| 61 | +Golo is not one of them, and neither are Go, Rust, Python or MoonBit. The language that *defines* an editor is registered by that editor, which is why a `.mbt` file opens as plain text here and a `.golo` file opens as plain text in Turbo MoonBit. | |
| 62 | + | |
| 63 | +That could have gone the other way. Putting all six scanners in the library would let any editor colour any of the languages, at no cost in dependencies — a Golo scanner is ordinary Go. It was rejected because it would mean the library grows a language every time somebody builds an editor, and because "what does this editor register?" would stop being the first question about a new one. | |
| 64 | + | |
| 65 | +## Why the scanner was not borrowed from GoloScript | |
| 66 | + | |
| 67 | +GoloScript is written in Go, and its `lexer` package is exactly the tokeniser this scanner reimplements. Turbo Go reaches for `go/scanner` in the same situation, so the question is fair. | |
| 68 | + | |
| 69 | +The answer is the module's name. GoloScript's `go.mod` declares `module golo`, a bare name with no host in it, and Go's module system cannot fetch a module by a path like that from anywhere: importing `golo/lexer` from another module needs a `replace` pointing at a checkout beside this one, and a committed `replace` is what `01-release.tag.sh` refuses to release. So the lexer's rules are carried here rather than called — and `lexer/lexer.go` and `token/token.go` are the specification the scanner is written against, line for line where it matters. The [colouring page](colouring-and-completion.md) names the places where that specification and the interpreter's own parser disagree. | |
| 70 | + | |
| 71 | +## Why the toolchain menu is `~G~olo` and not `~g~olo`, `gogolo` or `wagolo` | |
| 72 | + | |
| 73 | +The hot key was the easy part. Nine letters are taken by the fixed menus — F, E, S, R, C, O, W, N and H — and `G` is not one of them, so it lands on the first letter of the word, which costs nobody a second glance. Turbo Rust had no such luck and ended up on `Rus~t~`. | |
| 74 | + | |
| 75 | +The name was the real decision, and it went the same way every sibling's did. The menu holds whatever the project put in its tools file, and that is not always the interpreter: GoloScript itself is three binaries — `golo`, `gogolo`, `wagolo` — and the first tools file anybody writes outgrows all three, because a project's commands include containers, databases and a `Makefile` target somebody added in 2019. A menu called **golo** holding `wagolo build` is already a small lie, and one holding `docker compose up` is a large one. `Golo` is the language, and the language is what this editor is for. | |
| 76 | + | |
| 77 | +## Why the tests drive the real editor | |
| 78 | + | |
| 79 | +`internal/gololang/editor_test.go` builds a whole Turbo Golo on a simulated terminal — `app.New(screen, "turbo-classic", gololang.Profile())` — opens a file and checks the colouring, the menu bar and the hot keys. It uses only the library's public API. | |
| 80 | + | |
| 81 | +That is deliberate. The library's own suite proves the library works; what these tests prove is that *this editor is assembled correctly* — that `Register` was called, that the profile reached the menu bar, that a `.golo` file comes out coloured and a script with a `golo` shebang does too. A bug where `main` forgot to register Golo would pass every test in turbo-core. | |
| 82 | + | |
| 83 | +The same file drives a **real `golo lsp`** end to end, ten times over. It writes a script, opens it, starts the server, and then: | |
| 84 | + | |
| 85 | +- **types a function declaration that exists only in the buffer**, then types its first letters on another line and asks for a completion — `golo lsp` offers keywords and builtins for any file at all, so a completion holding `println` would prove nothing; one holding a function that is not on disk proves the buffer was sent; | |
| 86 | +- asks for the **completion of an empty prefix** and compares the answer with the scanner's own tables in both directions — every keyword and builtin the scanner colours must be one the server offers, and everything the server offers must be one the scanner knows, which is how the 157-entry builtin table is held to the interpreter rather than to memory; | |
| 87 | +- asks for the **definition** of a call and the **hover** over it, which comes back with the `#` comment written above the declaration; | |
| 88 | +- asks for the **file's symbols**; | |
| 89 | +- asks for the **references** of a call — the declaration and every call within the file — and for its **implementation**, which is the declaration itself, Golo having no interfaces; | |
| 90 | +- searches the **project for a symbol**, including one in a file the editor never opened; | |
| 91 | +- opens a file that **does not parse** and waits for a diagnostic to arrive unasked — the one feature whose failure looks exactly like success, because an editor with no error to show and one that cannot find the error are the same blank gutter; | |
| 92 | +- opens a file with a **C-style `//` comment** and waits for the lint that says Golo uses `#`. | |
| 93 | + | |
| 94 | +A last test pins what `golo lsp` still *cannot* do: it does not advertise `typeDefinition`, the documentation says so, and the test fails if a future golo starts answering — so the page gets revisited rather than quietly going stale. That is how the three tests before it came to exist: until GoloScript v0.2.0 the same test pinned references, implementations and project-wide symbols as refusals, and went red the day the server learnt them. | |
| 95 | + | |
| 96 | +## Rejected alternatives | |
| 97 | + | |
| 98 | +**Forking Turbo MoonBit.** The obvious way to get a fifth editor, and the reason the library exists instead: five copies of eleven thousand lines drift within a month, and every fix has to be made five times by somebody who remembers there are five. | |
| 99 | + | |
| 100 | +**A plugin system.** Turbo Golo is a Go program that imports a library. There is no dynamic loading and no ABI. Adding one would mean freezing the API of every package in turbo-core rather than of the handful a profile touches. | |
| 101 | + | |
| 102 | +**A configuration file instead of a profile.** The profile could have been TOML read at start-up, which would make a new editor a file rather than a program. It would also make the scanner inexpressible, and a half-configurable editor — everything but the colouring — is worse than either whole answer. | |
| 103 | + | |
| 104 | +**Importing GoloScript's lexer.** Weighed above: the module cannot be fetched, and a `replace` cannot be released. | |
| 105 | + | |
| 106 | +## How it relates to the rest | |
| 107 | + | |
| 108 | +- What each of the library's packages does: [turbo-core's package reference](https://rickub.com/turbo-editors/turbo-core/blob/main/docs/en/reference/packages.md) | |
| 109 | +- How the colouring works here: [Colouring and completion](colouring-and-completion.md) | |
| 110 | +- Why the tools menu is data: [Golo tools](golo-tools.md) | |
| 111 | +- The decisions that outlived the refactoring: [Design decisions](design-decisions.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,111 @@ | |||
| 1 | +# Architecture — explanation | ||
| 2 | + | ||
| 3 | +## What is this about? | ||
| 4 | + | ||
| 5 | +Turbo Golo is a command, a profile and a scanner. Everything else — the editing widget, the windows, the menus, the dialogs, the themes, the terminal emulator, the file tree, the LSP client — is [turbo-core](https://rickub.com/turbo-editors/turbo-core), the library every Turbo editor is built on. | ||
| 6 | + | ||
| 7 | +This page is about that split: what is here, what is there, and why the line falls where it does. | ||
| 8 | + | ||
| 9 | +## What is in this repository | ||
| 10 | + | ||
| 11 | +``` | ||
| 12 | +main.go flags, the terminal, and the wiring | ||
| 13 | +internal/gololang the whole of what makes this Turbo Golo | ||
| 14 | + gololang.go the profile: name, menu, server, where golo is installed | ||
| 15 | + scan.go the scanner's dispatcher, comments, what crosses a line | ||
| 16 | + literals.go the three quoted forms — "…", """…""" and '…' | ||
| 17 | + words.go numbers, keywords, the 157 builtins, the naming conventions | ||
| 18 | + templates.go three //go:embed declarations | ||
| 19 | + *.toml.tmpl the three starter files a project gets, embedded | ||
| 20 | +``` | ||
| 21 | + | ||
| 22 | +About a thousand lines counting the comments, of which some six hundred are the scanner — under five hundred lines of code by qlty's count, and a third of those are the builtin table. There is no `internal/app`, no `internal/ui`, no `internal/buffer` — those exist once, in the library, and all six editors use them unchanged. | ||
| 23 | + | ||
| 24 | +## What `main` does | ||
| 25 | + | ||
| 26 | +Six things, in this order: | ||
| 27 | + | ||
| 28 | +1. Parses the flags. | ||
| 29 | +2. Calls `gololang.Register()`, which teaches the library to colour `.golo` files and scripts whose first line names `golo`. | ||
| 30 | +3. Builds `gololang.Profile()` — the value that says this editor is Turbo Golo. | ||
| 31 | +4. Reads `.turbo-golo/settings.toml` from the working directory, if there is one. | ||
| 32 | +5. Opens the terminal and hands the screen, the theme name and the profile to `app.New`. | ||
| 33 | +6. Starts `golo lsp` in the directory of the file being edited, and runs the event loop. | ||
| 34 | + | ||
| 35 | +That is the whole command. Every decision it makes — which theme wins, which files to open, whether to start a language server — is about *this run*, not about Golo. | ||
| 36 | + | ||
| 37 | +## The profile is the seam | ||
| 38 | + | ||
| 39 | +```go | ||
| 40 | +profile.Profile{ | ||
| 41 | + Name: "Turbo Golo", | ||
| 42 | + Slug: "turbo-golo", | ||
| 43 | + Language: "Golo", | ||
| 44 | + ToolsMenu: "~G~olo", | ||
| 45 | + RootMarkers: nil, | ||
| 46 | + Server: profile.Server{Command: "golo", Args: []string{"lsp"}, …}, | ||
| 47 | + Templates: profile.Templates{Settings: …, Snippets: …, Tools: …}, | ||
| 48 | +} | ||
| 49 | +``` | ||
| 50 | + | ||
| 51 | +Everything that would otherwise be a hardcoded `"turbo-golo"`, `"golo"` or `".golo"` somewhere in eleven thousand lines is one field here. The library reads them; nothing in the library knows what any of them mean. | ||
| 52 | + | ||
| 53 | +`Slug` carries more than it looks. The binary is `turbo-golo`, the project directory is `.turbo-golo`, the user's own configuration lives in `~/.config/turbo-golo`, and the environment variables that override it are `TURBO_GOLO_THEME_DIR` and `TURBO_GOLO_SNIPPET_DIR` — all derived from that one word. | ||
| 54 | + | ||
| 55 | +`RootMarkers` is the one field that is empty here and full in every sibling. Go has `go.mod`, Rust `Cargo.toml`, Python `pyproject.toml`, MoonBit `moon.mod`; Golo has no manifest at all. A script is a file and a program is a directory of them, so there is nothing to walk up towards, and the library's `ProjectRoot` — given no markers — answers with the directory of the file. That is also all `golo lsp` needs: it answers about the file it is given and resolves imports from the modules embedded in the binary, never from disk. | ||
| 56 | + | ||
| 57 | +## Why the scanner is here and not in the library | ||
| 58 | + | ||
| 59 | +turbo-core colours eight languages itself: TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell. Those are the ones every editor meets whatever it is for — a project's configuration is TOML or YAML, its documentation is Markdown, its scripts are shell, its image build a Dockerfile. | ||
| 60 | + | ||
| 61 | +Golo is not one of them, and neither are Go, Rust, Python or MoonBit. The language that *defines* an editor is registered by that editor, which is why a `.mbt` file opens as plain text here and a `.golo` file opens as plain text in Turbo MoonBit. | ||
| 62 | + | ||
| 63 | +That could have gone the other way. Putting all six scanners in the library would let any editor colour any of the languages, at no cost in dependencies — a Golo scanner is ordinary Go. It was rejected because it would mean the library grows a language every time somebody builds an editor, and because "what does this editor register?" would stop being the first question about a new one. | ||
| 64 | + | ||
| 65 | +## Why the scanner was not borrowed from GoloScript | ||
| 66 | + | ||
| 67 | +GoloScript is written in Go, and its `lexer` package is exactly the tokeniser this scanner reimplements. Turbo Go reaches for `go/scanner` in the same situation, so the question is fair. | ||
| 68 | + | ||
| 69 | +The answer is the module's name. GoloScript's `go.mod` declares `module golo`, a bare name with no host in it, and Go's module system cannot fetch a module by a path like that from anywhere: importing `golo/lexer` from another module needs a `replace` pointing at a checkout beside this one, and a committed `replace` is what `01-release.tag.sh` refuses to release. So the lexer's rules are carried here rather than called — and `lexer/lexer.go` and `token/token.go` are the specification the scanner is written against, line for line where it matters. The [colouring page](colouring-and-completion.md) names the places where that specification and the interpreter's own parser disagree. | ||
| 70 | + | ||
| 71 | +## Why the toolchain menu is `~G~olo` and not `~g~olo`, `gogolo` or `wagolo` | ||
| 72 | + | ||
| 73 | +The hot key was the easy part. Nine letters are taken by the fixed menus — F, E, S, R, C, O, W, N and H — and `G` is not one of them, so it lands on the first letter of the word, which costs nobody a second glance. Turbo Rust had no such luck and ended up on `Rus~t~`. | ||
| 74 | + | ||
| 75 | +The name was the real decision, and it went the same way every sibling's did. The menu holds whatever the project put in its tools file, and that is not always the interpreter: GoloScript itself is three binaries — `golo`, `gogolo`, `wagolo` — and the first tools file anybody writes outgrows all three, because a project's commands include containers, databases and a `Makefile` target somebody added in 2019. A menu called **golo** holding `wagolo build` is already a small lie, and one holding `docker compose up` is a large one. `Golo` is the language, and the language is what this editor is for. | ||
| 76 | + | ||
| 77 | +## Why the tests drive the real editor | ||
| 78 | + | ||
| 79 | +`internal/gololang/editor_test.go` builds a whole Turbo Golo on a simulated terminal — `app.New(screen, "turbo-classic", gololang.Profile())` — opens a file and checks the colouring, the menu bar and the hot keys. It uses only the library's public API. | ||
| 80 | + | ||
| 81 | +That is deliberate. The library's own suite proves the library works; what these tests prove is that *this editor is assembled correctly* — that `Register` was called, that the profile reached the menu bar, that a `.golo` file comes out coloured and a script with a `golo` shebang does too. A bug where `main` forgot to register Golo would pass every test in turbo-core. | ||
| 82 | + | ||
| 83 | +The same file drives a **real `golo lsp`** end to end, ten times over. It writes a script, opens it, starts the server, and then: | ||
| 84 | + | ||
| 85 | +- **types a function declaration that exists only in the buffer**, then types its first letters on another line and asks for a completion — `golo lsp` offers keywords and builtins for any file at all, so a completion holding `println` would prove nothing; one holding a function that is not on disk proves the buffer was sent; | ||
| 86 | +- asks for the **completion of an empty prefix** and compares the answer with the scanner's own tables in both directions — every keyword and builtin the scanner colours must be one the server offers, and everything the server offers must be one the scanner knows, which is how the 157-entry builtin table is held to the interpreter rather than to memory; | ||
| 87 | +- asks for the **definition** of a call and the **hover** over it, which comes back with the `#` comment written above the declaration; | ||
| 88 | +- asks for the **file's symbols**; | ||
| 89 | +- asks for the **references** of a call — the declaration and every call within the file — and for its **implementation**, which is the declaration itself, Golo having no interfaces; | ||
| 90 | +- searches the **project for a symbol**, including one in a file the editor never opened; | ||
| 91 | +- opens a file that **does not parse** and waits for a diagnostic to arrive unasked — the one feature whose failure looks exactly like success, because an editor with no error to show and one that cannot find the error are the same blank gutter; | ||
| 92 | +- opens a file with a **C-style `//` comment** and waits for the lint that says Golo uses `#`. | ||
| 93 | + | ||
| 94 | +A last test pins what `golo lsp` still *cannot* do: it does not advertise `typeDefinition`, the documentation says so, and the test fails if a future golo starts answering — so the page gets revisited rather than quietly going stale. That is how the three tests before it came to exist: until GoloScript v0.2.0 the same test pinned references, implementations and project-wide symbols as refusals, and went red the day the server learnt them. | ||
| 95 | + | ||
| 96 | +## Rejected alternatives | ||
| 97 | + | ||
| 98 | +**Forking Turbo MoonBit.** The obvious way to get a fifth editor, and the reason the library exists instead: five copies of eleven thousand lines drift within a month, and every fix has to be made five times by somebody who remembers there are five. | ||
| 99 | + | ||
| 100 | +**A plugin system.** Turbo Golo is a Go program that imports a library. There is no dynamic loading and no ABI. Adding one would mean freezing the API of every package in turbo-core rather than of the handful a profile touches. | ||
| 101 | + | ||
| 102 | +**A configuration file instead of a profile.** The profile could have been TOML read at start-up, which would make a new editor a file rather than a program. It would also make the scanner inexpressible, and a half-configurable editor — everything but the colouring — is worse than either whole answer. | ||
| 103 | + | ||
| 104 | +**Importing GoloScript's lexer.** Weighed above: the module cannot be fetched, and a `replace` cannot be released. | ||
| 105 | + | ||
| 106 | +## How it relates to the rest | ||
| 107 | + | ||
| 108 | +- What each of the library's packages does: [turbo-core's package reference](https://rickub.com/turbo-editors/turbo-core/blob/main/docs/en/reference/packages.md) | ||
| 109 | +- How the colouring works here: [Colouring and completion](colouring-and-completion.md) | ||
| 110 | +- Why the tools menu is data: [Golo tools](golo-tools.md) | ||
| 111 | +- The decisions that outlived the refactoring: [Design decisions](design-decisions.md) | ||
added
docs/en/explanation/colouring-and-completion.md +118 -0 | new file mode 100644 | ||
| @@ -0,0 +1,118 @@ | ||
| 1 | +# Colouring and completion — explanation | |
| 2 | + | |
| 3 | +## What is this about? | |
| 4 | + | |
| 5 | +The two features that make Turbo Golo an editor *for Golo* rather than a text editor that happens to open `.golo` files: syntax colouring, and completion from a language server. They work quite differently, and the difference is instructive. | |
| 6 | + | |
| 7 | +## Colouring is ours; completion is not | |
| 8 | + | |
| 9 | +Colouring is done here, in about six hundred lines of hand-written Go. Completion is done by `golo lsp` — the interpreter itself, in language-server mode — and Turbo Golo only asks and draws. | |
| 10 | + | |
| 11 | +That split is not an accident of effort. Colouring has to be **instant and tolerant**: it runs on every keystroke, on text that is invalid most of the time it is being typed, and a highlighter that stops to think or gives up on broken input is worse than no highlighter. Completion has to be **correct**, which for Golo means parsing the file, following its `import` lines into the modules embedded in the binary, and knowing what every one of 157 builtins takes — and nothing that has to be instant can also be that. | |
| 12 | + | |
| 13 | +So the editor draws colours it computed itself, and shows completions somebody else computed. | |
| 14 | + | |
| 15 | +## Why Golo is scanned by hand, with a lexer sitting right there | |
| 16 | + | |
| 17 | +GoloScript is written in Go, and its `lexer` package is a tokeniser for exactly this language. Turbo Go goes through `go/scanner` in the same situation — the standard library analysing its own language, so the editor and the compiler agree about what a token is with nothing to keep in step. The obvious move was to import `golo/lexer` and convert its offsets with the library's `LineIndex`. | |
| 18 | + | |
| 19 | +It cannot be imported. GoloScript's `go.mod` says `module golo`: a bare name, no host, and Go's module system has no way to fetch a module by such a path. Importing it needs a `replace` directive pointing at a checkout beside this one, and a committed `replace` breaks every clone that has no such checkout — which is why the release script refuses to tag one. Vendoring the two packages was the other route, and it would mean a copy of somebody else's lexer that stops being theirs the day they change it. | |
| 20 | + | |
| 21 | +So the lexer is the **specification** rather than a dependency. `lexer/lexer.go` and `token/token.go` say what a token is — which runes open a comment, how a number ends, which words are reserved — and this scanner says the same thing in the library's `LineScanner` style. Where the two could disagree, the lexer's line is quoted in the code beside the decision. | |
| 22 | + | |
| 23 | +The scanner it is. Some six hundred lines, one file each for the dispatcher, the literals and the words — and no attempt at a general engine. There is no pattern language, no grammar format and no table of regular expressions: it is ordinary Go that a reader can follow, which is the same rule the eight scanners in turbo-core follow. | |
| 24 | + | |
| 25 | +## What crosses a line break, and why it is carried rather than cut | |
| 26 | + | |
| 27 | +Four constructs may run from one line to the next, and the interpreter's lexer is the authority on each: | |
| 28 | + | |
| 29 | +- **A block comment** runs from one `----` to the next, wherever that is. Three dashes are two minus signs and a third; a fifth dash is part of the text. | |
| 30 | +- **A string** runs to its closing quote. The lexer reads it with `for l.ch != '"' && l.ch != 0`, which stops at the quote or the end of the file and at nothing in between — a newline inside a string is part of the string. | |
| 31 | +- **A `"""` multi-line string** runs to the next three quotes, with no escapes considered on the way. | |
| 32 | +- **A `'…'` character literal** is read by the same loop as a string, and so behaves the same way. | |
| 33 | + | |
| 34 | +Every other editor in this family stops a literal at the end of its line when the closing quote is missing, and Turbo MoonBit makes a point of it: MoonBit's grammar says a newline before the closing quote is an *error*, so there is nothing to carry. Golo's lexer says the opposite, and the scanner follows the lexer: **an unterminated string paints the rest of the file until a quote turns up**, because that is exactly what the interpreter will read as string. The colour is not a warning, it is a statement about what the program means — and a screenful of green after a stray quote is that statement made visible. | |
| 35 | + | |
| 36 | +What is carried is a value saying *which* of the four is open. None of them nests, so a depth would be a claim the language does not make. | |
| 37 | + | |
| 38 | +## Where the scanner leans on the language, and where on convention | |
| 39 | + | |
| 40 | +**Keywords, constants and builtins are tables read out of GoloScript, not remembered.** The 38 keywords are `token/token.go`'s table less the three literals; the 157 builtins are what `evaluator.BuiltinNames()` answers less the five test counters that begin with a double underscore — the same five the language server keeps out of its completions. A test holds that table to a running `golo lsp` in both directions, which is how it stays a table read from the toolchain rather than a table somebody once typed. | |
| 41 | + | |
| 42 | +**A capitalised name is a type, and here that is a convention rather than a rule.** Golo has no case rule in its lexer: `let Count = 1` is legal. But structs, unions and their variants are capitalised by everybody — `Point`, `Shape`, `Circle`, `Some`, `None` — and nothing else customarily is, so the scanner colours by the convention, as Turbo Python does with PEP 8. What that costs is that a variant's constructor is coloured as a type (`Circle(1.0)` and `Result_Failure("no")` look like types applied to arguments) and a capitalised variable is coloured as one too. Nothing in the syntax separates them. | |
| 43 | + | |
| 44 | +**`Some`, `None`, `Ok` and `Err` are types here, not constants.** Turbo Rust and Turbo MoonBit colour them as constants because a reader meets them everywhere and reads them as built in. In Golo they are not: they are the variants of ordinary unions declared in `gololang.Errors`, available only after `import gololang.Errors`, and colouring them as the language's own would tell a reader they need no import when they do. | |
| 45 | + | |
| 46 | +**The name after `function` is a function, and the path after `module` or `import` is one name.** Everywhere else a name is a function because a parenthesis follows it, and a declaration — `function main = |args|` — is the one place that is not true; without a special case every function a file declares would be coloured as an ordinary variable at the one place a reader looks for it. A module path — `hello.World`, `gololang.Errors` — is one span and one colour because it is one name, and the reading it has to be saved from is the one where `gololang.Errors` looks like a variable with something done to it. | |
| 47 | + | |
| 48 | +**Names may be almost anything.** The lexer's `isLetter` admits any Unicode letter or mark, an underscore, and four blocks of emoji, so `let 😀 = 1` and `function 🚀launch = …` are legal Golo. The scanner uses the same predicate rather than turbo-core's ASCII one, so they are coloured — and so are `été` and `名前`, which Turbo MoonBit leaves uncoloured for its own language. | |
| 49 | + | |
| 50 | +## What the lexer reads and the parser refuses | |
| 51 | + | |
| 52 | +This is the boundary worth stating plainly, because it is not one the scanner can see. GoloScript's lexer and its parser were written at different times, and the lexer is ahead: it reads several tokens that the parser, as of v0.1.1, then rejects. | |
| 53 | + | |
| 54 | +| The lexer reads | The parser says | | |
| 55 | +| --- | --- | | |
| 56 | +| `42L`, a long | `could not parse "42L" as integer` | | |
| 57 | +| `3.14F`, `2.0f`, a float | `could not parse "3.14F" as float` | | |
| 58 | +| `'x'`, a character | `no prefix parse function for CHAR found` | | |
| 59 | +| `1..3`, a range | `expected next token to be ), got .. instead` | | |
| 60 | +| `orIfNull`, `oftype` | `expected next token to be ), got orIfNull instead` | | |
| 61 | +| `local function …` | `no prefix parse function for LOCAL found` | | |
| 62 | + | |
| 63 | +The scanner colours what the lexer reads, because the lexer is the specification of what a token *is* and the parser's opinion of what to do with it may change tomorrow. So `42L` is one number and `orIfNull` is a keyword, and **a coloured token is not a promise that the interpreter accepts it**. The language server tells you when it does not: open `demos/syntax-tour/lexer-only.golo` and every one of those lines gets a mark in the gutter. | |
| 64 | + | |
| 65 | +## What the scanner refuses to guess | |
| 66 | + | |
| 67 | +Where a construct cannot be recognised from what one line holds, it is left alone rather than approximated. A highlighter that is wrong is worse than one that is quiet: | |
| 68 | + | |
| 69 | +| Not recognised | Because | | |
| 70 | +| --- | --- | | |
| 71 | +| Digit separators and other bases | The lexer has no `1_000`, no `0xFF`, no `0b1010`. `1_000` is the number `1` followed by the name `_000`, and `0xFF` is `0` followed by `xFF` — which is what the interpreter sees, and colouring either as one number would be inventing a literal it rejects | | |
| 72 | +| A leading dot as a number | The lexer requires a digit before the point, so `.5` is a dot and then `5` | | |
| 73 | +| A lower-case `l` as a long suffix | The lexer accepts only `L`; `42l` is `42` and the name `l` | | |
| 74 | +| A keyword used after a colon as a method name | `obj: match()` keeps `match` a keyword. The scanner does not track what a colon introduces, and the lexer would refuse the word anyway | | |
| 75 | +| Escapes inside `"""…"""` | The lexer appends every rune until the three quotes, so `"""a\"""` ends at the first `"""` whatever the backslash meant | | |
| 76 | +| Whether a name is bound in this scope | Nothing here reads more than one line at a time; that is the language server's question, and [F1 answers it](../how-to/ask-about-code.md) | | |
| 77 | + | |
| 78 | +## The other eight languages come free | |
| 79 | + | |
| 80 | +TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell are coloured by turbo-core, not here. A Golo project has a `README.md`, a `compose.yaml` for the service it talks to, a Dockerfile to ship as, and an editor that coloured only the `.golo` files would make you leave it for the rest. | |
| 81 | + | |
| 82 | +That they are shared rather than copied is the point of the library: they were written once, for Turbo Go, and Turbo Golo got them by importing a package. | |
| 83 | + | |
| 84 | +## Completion, and why it can fail silently | |
| 85 | + | |
| 86 | +Turbo Golo knows nothing about Golo's semantics and does not try to. It asks `golo lsp` over the Language Server Protocol and draws the answer. | |
| 87 | + | |
| 88 | +Three things about that are worth knowing, because all three look like "completion is broken": | |
| 89 | + | |
| 90 | +**The server is the interpreter.** There is no separate `golo-lsp` binary to install and no toolchain it depends on: `golo lsp` reuses the interpreter's lexer, parser and AST. So "no completion" on a machine that runs Golo scripts has exactly one cause — the editor cannot find `golo` — and the status bar says so, with the address of the release page. | |
| 91 | + | |
| 92 | +**Only top-level declarations are offered.** The server's completion lists keywords, builtins, the functions and unions declared at the top level of the file, and the symbols pulled in by `import` from the modules embedded in the binary. A function you declared inside another function's body is not in the list, and nor is anything from a `.golo` file of your own on disk: imports of user modules are not resolved. That is the server's design, and it is written down rather than worked around. | |
| 93 | + | |
| 94 | +**Diagnostics are about parsing, not running.** `golo lsp` publishes syntax errors and two lints — a `:`/`.` confusion, and C-style `//` or `/* */` comments where Golo wants `#` and `----`. A program that parses and then fails at run time gets no mark, because the server never runs it. And a syntax error highlights a whole line: the parser's messages carry a line number and no column, so the mark lands on the line. | |
| 95 | + | |
| 96 | +The editor's answer to the first is [Run ▸ Language server status](../reference/menus.md), which says what it found, where it started it and whether it is ready — because "nothing happened" is not something a user can act on. | |
| 97 | + | |
| 98 | +## Nine questions, one connection — and the four `golo lsp` does not answer | |
| 99 | + | |
| 100 | +Completion is the loudest thing the language server does and the least revealing. The same connection asks eight more questions, and they divide into three kinds by what comes back. | |
| 101 | + | |
| 102 | +**Something to read.** `hover` — what is this? — drawn in a box. For a function you declared, that is the `#` comments written immediately above it; for a builtin, its signature and a worked example; for a keyword, a sentence. | |
| 103 | + | |
| 104 | +**Places in the code.** `definition`, `typeDefinition`, `implementation`, `references`. One request each, one answer shape between them, which is why they are one function underneath. A single place is opened; several are offered as a list, because a single answer is the exception rather than the rule — and for a long time this family's editors took the first and threw the rest away. | |
| 105 | + | |
| 106 | +**Names.** `documentSymbol` for a file's own outline, `workspace/symbol` for a search across the project. The protocol has three shapes for a symbol and the editor wants one, so the flattening is done where the answers arrive rather than where they are drawn. | |
| 107 | + | |
| 108 | +And one thing nobody asks for at all: **`publishDiagnostics` arrives unbidden**, whenever the server has an opinion, on open and on every edit. That is why the mark in the gutter appears without anything being pressed. | |
| 109 | + | |
| 110 | +**One of the nine comes back empty with `golo lsp`, and that is the server's boundary rather than the editor's.** It advertises `completion`, `hover`, `definition`, `documentSymbol`, `references`, `implementation` and `workspaceSymbol` — and not `typeDefinition`, so **Code ▸ Type definition** reports nothing found. Until GoloScript v0.2.0 it advertised only the first four, and **Shift-F12** (references), **Code ▸ Find implementations** and **Ctrl-T** (a symbol anywhere in the project) came back empty too; the test that pins this boundary failed the day the server started answering them, and this paragraph was revised — which is what the test is for. The gap is written down rather than hidden because the alternative — greying out a menu item depending on what a server said at start-up — makes the menu a different shape on different machines, and a user who has read this page knows more than one who found a greyed item. | |
| 111 | + | |
| 112 | +The editor asks for none of this until the server says it is ready, and says which of those it is when a question cannot be answered. "Nothing found" and "I have not finished loading" are the same empty answer and very different news; conflating them is the most confusing way completion has ever failed here. | |
| 113 | + | |
| 114 | +## How it relates to the rest | |
| 115 | + | |
| 116 | +- Exactly what is recognised: [Languages coloured](../reference/languages.md) | |
| 117 | +- Getting completion working: [How to enable Golo completion](../how-to/enable-completion.md) | |
| 118 | +- Where the scanner lives and why: [Architecture](architecture.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | +# Colouring and completion — explanation | ||
| 2 | + | ||
| 3 | +## What is this about? | ||
| 4 | + | ||
| 5 | +The two features that make Turbo Golo an editor *for Golo* rather than a text editor that happens to open `.golo` files: syntax colouring, and completion from a language server. They work quite differently, and the difference is instructive. | ||
| 6 | + | ||
| 7 | +## Colouring is ours; completion is not | ||
| 8 | + | ||
| 9 | +Colouring is done here, in about six hundred lines of hand-written Go. Completion is done by `golo lsp` — the interpreter itself, in language-server mode — and Turbo Golo only asks and draws. | ||
| 10 | + | ||
| 11 | +That split is not an accident of effort. Colouring has to be **instant and tolerant**: it runs on every keystroke, on text that is invalid most of the time it is being typed, and a highlighter that stops to think or gives up on broken input is worse than no highlighter. Completion has to be **correct**, which for Golo means parsing the file, following its `import` lines into the modules embedded in the binary, and knowing what every one of 157 builtins takes — and nothing that has to be instant can also be that. | ||
| 12 | + | ||
| 13 | +So the editor draws colours it computed itself, and shows completions somebody else computed. | ||
| 14 | + | ||
| 15 | +## Why Golo is scanned by hand, with a lexer sitting right there | ||
| 16 | + | ||
| 17 | +GoloScript is written in Go, and its `lexer` package is a tokeniser for exactly this language. Turbo Go goes through `go/scanner` in the same situation — the standard library analysing its own language, so the editor and the compiler agree about what a token is with nothing to keep in step. The obvious move was to import `golo/lexer` and convert its offsets with the library's `LineIndex`. | ||
| 18 | + | ||
| 19 | +It cannot be imported. GoloScript's `go.mod` says `module golo`: a bare name, no host, and Go's module system has no way to fetch a module by such a path. Importing it needs a `replace` directive pointing at a checkout beside this one, and a committed `replace` breaks every clone that has no such checkout — which is why the release script refuses to tag one. Vendoring the two packages was the other route, and it would mean a copy of somebody else's lexer that stops being theirs the day they change it. | ||
| 20 | + | ||
| 21 | +So the lexer is the **specification** rather than a dependency. `lexer/lexer.go` and `token/token.go` say what a token is — which runes open a comment, how a number ends, which words are reserved — and this scanner says the same thing in the library's `LineScanner` style. Where the two could disagree, the lexer's line is quoted in the code beside the decision. | ||
| 22 | + | ||
| 23 | +The scanner it is. Some six hundred lines, one file each for the dispatcher, the literals and the words — and no attempt at a general engine. There is no pattern language, no grammar format and no table of regular expressions: it is ordinary Go that a reader can follow, which is the same rule the eight scanners in turbo-core follow. | ||
| 24 | + | ||
| 25 | +## What crosses a line break, and why it is carried rather than cut | ||
| 26 | + | ||
| 27 | +Four constructs may run from one line to the next, and the interpreter's lexer is the authority on each: | ||
| 28 | + | ||
| 29 | +- **A block comment** runs from one `----` to the next, wherever that is. Three dashes are two minus signs and a third; a fifth dash is part of the text. | ||
| 30 | +- **A string** runs to its closing quote. The lexer reads it with `for l.ch != '"' && l.ch != 0`, which stops at the quote or the end of the file and at nothing in between — a newline inside a string is part of the string. | ||
| 31 | +- **A `"""` multi-line string** runs to the next three quotes, with no escapes considered on the way. | ||
| 32 | +- **A `'…'` character literal** is read by the same loop as a string, and so behaves the same way. | ||
| 33 | + | ||
| 34 | +Every other editor in this family stops a literal at the end of its line when the closing quote is missing, and Turbo MoonBit makes a point of it: MoonBit's grammar says a newline before the closing quote is an *error*, so there is nothing to carry. Golo's lexer says the opposite, and the scanner follows the lexer: **an unterminated string paints the rest of the file until a quote turns up**, because that is exactly what the interpreter will read as string. The colour is not a warning, it is a statement about what the program means — and a screenful of green after a stray quote is that statement made visible. | ||
| 35 | + | ||
| 36 | +What is carried is a value saying *which* of the four is open. None of them nests, so a depth would be a claim the language does not make. | ||
| 37 | + | ||
| 38 | +## Where the scanner leans on the language, and where on convention | ||
| 39 | + | ||
| 40 | +**Keywords, constants and builtins are tables read out of GoloScript, not remembered.** The 38 keywords are `token/token.go`'s table less the three literals; the 157 builtins are what `evaluator.BuiltinNames()` answers less the five test counters that begin with a double underscore — the same five the language server keeps out of its completions. A test holds that table to a running `golo lsp` in both directions, which is how it stays a table read from the toolchain rather than a table somebody once typed. | ||
| 41 | + | ||
| 42 | +**A capitalised name is a type, and here that is a convention rather than a rule.** Golo has no case rule in its lexer: `let Count = 1` is legal. But structs, unions and their variants are capitalised by everybody — `Point`, `Shape`, `Circle`, `Some`, `None` — and nothing else customarily is, so the scanner colours by the convention, as Turbo Python does with PEP 8. What that costs is that a variant's constructor is coloured as a type (`Circle(1.0)` and `Result_Failure("no")` look like types applied to arguments) and a capitalised variable is coloured as one too. Nothing in the syntax separates them. | ||
| 43 | + | ||
| 44 | +**`Some`, `None`, `Ok` and `Err` are types here, not constants.** Turbo Rust and Turbo MoonBit colour them as constants because a reader meets them everywhere and reads them as built in. In Golo they are not: they are the variants of ordinary unions declared in `gololang.Errors`, available only after `import gololang.Errors`, and colouring them as the language's own would tell a reader they need no import when they do. | ||
| 45 | + | ||
| 46 | +**The name after `function` is a function, and the path after `module` or `import` is one name.** Everywhere else a name is a function because a parenthesis follows it, and a declaration — `function main = |args|` — is the one place that is not true; without a special case every function a file declares would be coloured as an ordinary variable at the one place a reader looks for it. A module path — `hello.World`, `gololang.Errors` — is one span and one colour because it is one name, and the reading it has to be saved from is the one where `gololang.Errors` looks like a variable with something done to it. | ||
| 47 | + | ||
| 48 | +**Names may be almost anything.** The lexer's `isLetter` admits any Unicode letter or mark, an underscore, and four blocks of emoji, so `let 😀 = 1` and `function 🚀launch = …` are legal Golo. The scanner uses the same predicate rather than turbo-core's ASCII one, so they are coloured — and so are `été` and `名前`, which Turbo MoonBit leaves uncoloured for its own language. | ||
| 49 | + | ||
| 50 | +## What the lexer reads and the parser refuses | ||
| 51 | + | ||
| 52 | +This is the boundary worth stating plainly, because it is not one the scanner can see. GoloScript's lexer and its parser were written at different times, and the lexer is ahead: it reads several tokens that the parser, as of v0.1.1, then rejects. | ||
| 53 | + | ||
| 54 | +| The lexer reads | The parser says | | ||
| 55 | +| --- | --- | | ||
| 56 | +| `42L`, a long | `could not parse "42L" as integer` | | ||
| 57 | +| `3.14F`, `2.0f`, a float | `could not parse "3.14F" as float` | | ||
| 58 | +| `'x'`, a character | `no prefix parse function for CHAR found` | | ||
| 59 | +| `1..3`, a range | `expected next token to be ), got .. instead` | | ||
| 60 | +| `orIfNull`, `oftype` | `expected next token to be ), got orIfNull instead` | | ||
| 61 | +| `local function …` | `no prefix parse function for LOCAL found` | | ||
| 62 | + | ||
| 63 | +The scanner colours what the lexer reads, because the lexer is the specification of what a token *is* and the parser's opinion of what to do with it may change tomorrow. So `42L` is one number and `orIfNull` is a keyword, and **a coloured token is not a promise that the interpreter accepts it**. The language server tells you when it does not: open `demos/syntax-tour/lexer-only.golo` and every one of those lines gets a mark in the gutter. | ||
| 64 | + | ||
| 65 | +## What the scanner refuses to guess | ||
| 66 | + | ||
| 67 | +Where a construct cannot be recognised from what one line holds, it is left alone rather than approximated. A highlighter that is wrong is worse than one that is quiet: | ||
| 68 | + | ||
| 69 | +| Not recognised | Because | | ||
| 70 | +| --- | --- | | ||
| 71 | +| Digit separators and other bases | The lexer has no `1_000`, no `0xFF`, no `0b1010`. `1_000` is the number `1` followed by the name `_000`, and `0xFF` is `0` followed by `xFF` — which is what the interpreter sees, and colouring either as one number would be inventing a literal it rejects | | ||
| 72 | +| A leading dot as a number | The lexer requires a digit before the point, so `.5` is a dot and then `5` | | ||
| 73 | +| A lower-case `l` as a long suffix | The lexer accepts only `L`; `42l` is `42` and the name `l` | | ||
| 74 | +| A keyword used after a colon as a method name | `obj: match()` keeps `match` a keyword. The scanner does not track what a colon introduces, and the lexer would refuse the word anyway | | ||
| 75 | +| Escapes inside `"""…"""` | The lexer appends every rune until the three quotes, so `"""a\"""` ends at the first `"""` whatever the backslash meant | | ||
| 76 | +| Whether a name is bound in this scope | Nothing here reads more than one line at a time; that is the language server's question, and [F1 answers it](../how-to/ask-about-code.md) | | ||
| 77 | + | ||
| 78 | +## The other eight languages come free | ||
| 79 | + | ||
| 80 | +TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell are coloured by turbo-core, not here. A Golo project has a `README.md`, a `compose.yaml` for the service it talks to, a Dockerfile to ship as, and an editor that coloured only the `.golo` files would make you leave it for the rest. | ||
| 81 | + | ||
| 82 | +That they are shared rather than copied is the point of the library: they were written once, for Turbo Go, and Turbo Golo got them by importing a package. | ||
| 83 | + | ||
| 84 | +## Completion, and why it can fail silently | ||
| 85 | + | ||
| 86 | +Turbo Golo knows nothing about Golo's semantics and does not try to. It asks `golo lsp` over the Language Server Protocol and draws the answer. | ||
| 87 | + | ||
| 88 | +Three things about that are worth knowing, because all three look like "completion is broken": | ||
| 89 | + | ||
| 90 | +**The server is the interpreter.** There is no separate `golo-lsp` binary to install and no toolchain it depends on: `golo lsp` reuses the interpreter's lexer, parser and AST. So "no completion" on a machine that runs Golo scripts has exactly one cause — the editor cannot find `golo` — and the status bar says so, with the address of the release page. | ||
| 91 | + | ||
| 92 | +**Only top-level declarations are offered.** The server's completion lists keywords, builtins, the functions and unions declared at the top level of the file, and the symbols pulled in by `import` from the modules embedded in the binary. A function you declared inside another function's body is not in the list, and nor is anything from a `.golo` file of your own on disk: imports of user modules are not resolved. That is the server's design, and it is written down rather than worked around. | ||
| 93 | + | ||
| 94 | +**Diagnostics are about parsing, not running.** `golo lsp` publishes syntax errors and two lints — a `:`/`.` confusion, and C-style `//` or `/* */` comments where Golo wants `#` and `----`. A program that parses and then fails at run time gets no mark, because the server never runs it. And a syntax error highlights a whole line: the parser's messages carry a line number and no column, so the mark lands on the line. | ||
| 95 | + | ||
| 96 | +The editor's answer to the first is [Run ▸ Language server status](../reference/menus.md), which says what it found, where it started it and whether it is ready — because "nothing happened" is not something a user can act on. | ||
| 97 | + | ||
| 98 | +## Nine questions, one connection — and the four `golo lsp` does not answer | ||
| 99 | + | ||
| 100 | +Completion is the loudest thing the language server does and the least revealing. The same connection asks eight more questions, and they divide into three kinds by what comes back. | ||
| 101 | + | ||
| 102 | +**Something to read.** `hover` — what is this? — drawn in a box. For a function you declared, that is the `#` comments written immediately above it; for a builtin, its signature and a worked example; for a keyword, a sentence. | ||
| 103 | + | ||
| 104 | +**Places in the code.** `definition`, `typeDefinition`, `implementation`, `references`. One request each, one answer shape between them, which is why they are one function underneath. A single place is opened; several are offered as a list, because a single answer is the exception rather than the rule — and for a long time this family's editors took the first and threw the rest away. | ||
| 105 | + | ||
| 106 | +**Names.** `documentSymbol` for a file's own outline, `workspace/symbol` for a search across the project. The protocol has three shapes for a symbol and the editor wants one, so the flattening is done where the answers arrive rather than where they are drawn. | ||
| 107 | + | ||
| 108 | +And one thing nobody asks for at all: **`publishDiagnostics` arrives unbidden**, whenever the server has an opinion, on open and on every edit. That is why the mark in the gutter appears without anything being pressed. | ||
| 109 | + | ||
| 110 | +**One of the nine comes back empty with `golo lsp`, and that is the server's boundary rather than the editor's.** It advertises `completion`, `hover`, `definition`, `documentSymbol`, `references`, `implementation` and `workspaceSymbol` — and not `typeDefinition`, so **Code ▸ Type definition** reports nothing found. Until GoloScript v0.2.0 it advertised only the first four, and **Shift-F12** (references), **Code ▸ Find implementations** and **Ctrl-T** (a symbol anywhere in the project) came back empty too; the test that pins this boundary failed the day the server started answering them, and this paragraph was revised — which is what the test is for. The gap is written down rather than hidden because the alternative — greying out a menu item depending on what a server said at start-up — makes the menu a different shape on different machines, and a user who has read this page knows more than one who found a greyed item. | ||
| 111 | + | ||
| 112 | +The editor asks for none of this until the server says it is ready, and says which of those it is when a question cannot be answered. "Nothing found" and "I have not finished loading" are the same empty answer and very different news; conflating them is the most confusing way completion has ever failed here. | ||
| 113 | + | ||
| 114 | +## How it relates to the rest | ||
| 115 | + | ||
| 116 | +- Exactly what is recognised: [Languages coloured](../reference/languages.md) | ||
| 117 | +- Getting completion working: [How to enable Golo completion](../how-to/enable-completion.md) | ||
| 118 | +- Where the scanner lives and why: [Architecture](architecture.md) | ||
added
docs/en/explanation/design-decisions.md +121 -0 | new file mode 100644 | ||
| @@ -0,0 +1,121 @@ | ||
| 1 | +# Design decisions — explanation | |
| 2 | + | |
| 3 | +## What is this about? | |
| 4 | + | |
| 5 | +The choices that shaped Turbo Golo, what the alternatives were, and why they were turned down. Most of them were made once, in [turbo-core](https://rickub.com/turbo-editors/turbo-core), and every editor of the family inherits them; the ones about the language server and the toolchain are this editor's own. This is the page to read before changing something that looks arbitrary. | |
| 6 | + | |
| 7 | +## Two dependencies, and no more | |
| 8 | + | |
| 9 | +Turbo Golo depends on turbo-core, and through it on `tcell/v2` and `BurntSushi/toml`. Everything else is the standard library — including the tokeniser, the JSON-RPC client, the LSP framing and the file handling. | |
| 10 | + | |
| 11 | +**What was rejected.** `go.lsp.dev/jsonrpc2` would have saved a few hundred lines of the library's `lsp` package. `rivo/tview` would have saved rather more of its `ui`. A syntax-highlighting library would have brought fifty languages instead of one. | |
| 12 | + | |
| 13 | +**Why.** An editor is a program you keep for years and change often. Every dependency is a piece of it you cannot change, cannot fully test, and have to track. The protocol is simple enough to write down, and writing it down put the whole conversation somewhere a reader can follow. Three hundred lines you understand beat three hundred you inherit. | |
| 14 | + | |
| 15 | +The exception proves the rule: `tcell` is not a convenience, it is the terminal-compatibility database, and reimplementing that would be neither small nor honest work. | |
| 16 | + | |
| 17 | +## The widget framework is hand-written | |
| 18 | + | |
| 19 | +`tview` has widgets. `bubbletea` has an architecture. Neither has what Turbo Vision had: overlapping movable windows with shadows, a menu bar with hot keys, and modal dialogs, all drawn with box characters in sixteen colours. | |
| 20 | + | |
| 21 | +The Elm-style architecture that `bubbletea` uses re-renders the whole view on every message. That model is excellent for a form and awkward for a full-screen editor with windows stacked on top of each other and a cursor that has to be in one exact cell. | |
| 22 | + | |
| 23 | +Writing the framework cost roughly fifteen hundred lines. In exchange the editor looks like Turbo C rather than like a modern TUI wearing a blue background, and every drawing decision is one file away. | |
| 24 | + | |
| 25 | +## Bounds are absolute screen coordinates | |
| 26 | + | |
| 27 | +Every widget's `Bounds()` is where it really is on the terminal, not where it is relative to its parent. Hit-testing a mouse click is then a plain rectangle test, and no event ever needs translating on its way down. | |
| 28 | + | |
| 29 | +**The cost** is that containers place their children in screen space. **The alternative** — relative coordinates with a translation at each hop — moves the arithmetic from layout into event handling, where it is done far more often and is far easier to get wrong. Clipping still composes correctly because a painter intersects its parent's clip, so a child with wrong arithmetic draws nothing rather than drawing over its neighbours. | |
| 30 | + | |
| 31 | +## Every change goes through one function | |
| 32 | + | |
| 33 | +`buffer.ReplaceRange` is the only place the text is modified. Insert, backspace, delete, indent, paste and undo all funnel through it, and it is the only place the undo history, the modified flag, the revision counter and the cursor are maintained. | |
| 34 | + | |
| 35 | +The alternative — each operation maintaining its own bookkeeping — is how undo bugs are born. There is exactly one thing to get right, and it is tested directly. | |
| 36 | + | |
| 37 | +## Windows follow the terminal, they do not scale with it | |
| 38 | + | |
| 39 | +A window has a **grow mode**, which names the desktop edges it follows. A document window follows the right and bottom edges: its top-left corner stays where it is, and its far corner moves by exactly as much as the terminal's did. A window that filled the terminal therefore still fills it, and one you had cascaded keeps its offset. | |
| 40 | + | |
| 41 | +**The alternative was proportional scaling** — multiply every window's rectangle by the ratio of the old and new sizes. It was rejected because it moves windows the user deliberately placed, and because rounding makes it lossy: shrink and grow again and nothing is where it was. Turbo Vision used grow modes, and they are still the right answer. | |
| 42 | + | |
| 43 | +Whatever its grow mode, a window is then held to the desktop's own size. One larger than the desktop that holds it has parts nobody can reach. | |
| 44 | + | |
| 45 | +## A window's boxes say what they will do, not what the window is | |
| 46 | + | |
| 47 | +The frame carries two boxes: `[x]` at the left closes the window, `[■]` at the right fills the desktop. | |
| 48 | + | |
| 49 | +The close box used to be `[■]` — Turbo Vision's own — and it had to move. Two boxes on one frame need to be told apart at a glance, and a filled block reads as "fill the screen" far more readily than as "close". `[x]` is what a close button has meant for thirty years; the block went to the job it actually looks like. | |
| 50 | + | |
| 51 | +The maximise box **changes with the window's state**: `[■]` while there is room to grow, `[▬]` once the window fills the desktop. The alternative was a fixed symbol, and it makes the button ambiguous exactly when you need it — you can see that the window is large, but not whether pressing the box will make it larger still or put it back. A control that shows its *current state* leaves you to work out the action; one that shows its *action* does not. | |
| 52 | + | |
| 53 | +A window with nowhere to maximise into shows **no box at all**, rather than one that does nothing. Only the desktop knows what area a window would fill, so a window that is not on one has nothing to offer. | |
| 54 | + | |
| 55 | +**Window ▸ Maximise is the same toggle**, not a one-way action. A menu item and a button that disagreed about what "maximise" means would be a bug people reported rather than a subtlety they appreciated. | |
| 56 | + | |
| 57 | +## Undo merges runs of typing | |
| 58 | + | |
| 59 | +Typing `function` and pressing Ctrl-Z removes all eight letters. So does a run of backspaces. Moving the cursor ends the run, and typing never merges with deleting. | |
| 60 | + | |
| 61 | +Character-by-character undo is what a naive implementation gives you, and it is what Turbo C itself did. It is also what nobody wants any more. | |
| 62 | + | |
| 63 | +## Themes are TOML, with two kinds of inheritance | |
| 64 | + | |
| 65 | +**Between files**, `inherits` takes the parent's resolved styles as the starting point. A theme of your own can therefore be five lines. | |
| 66 | + | |
| 67 | +**Between keys**, along the dots: `syntax.keyword` falls back to `syntax`, and `syntax` to `default`. This happens twice — once at parse time, so an entry setting only `fg` inherits its `bg`, and once at lookup time, so a theme that never mentions `syntax.keyword` still colours keywords. | |
| 68 | + | |
| 69 | +That second one is what makes a partial theme a usable theme, and it is why there is no such thing as a theme that leaves half the screen unpainted. | |
| 70 | + | |
| 71 | +**Why TOML rather than JSON.** Comments. A theme is a file people edit by hand and annotate. | |
| 72 | + | |
| 73 | +**An unknown colour is an error**, not a silent fallback to the terminal default. A typo that quietly repaints half the screen is much harder to find than one that says so at load. | |
| 74 | + | |
| 75 | +## The language server is optional by construction | |
| 76 | + | |
| 77 | +`app.Language` wraps the whole `golo lsp` conversation, and when there is no server every method is a no-op rather than an error. Nothing else in the editor asks whether a language server exists. | |
| 78 | + | |
| 79 | +The alternative — checking for `nil` at every call site — is that many chances to forget. Here, forgetting is impossible: there is nothing to check. | |
| 80 | + | |
| 81 | +This is why `golo` is not bundled, not downloaded, and not required. It is looked for on `PATH` and then in `/usr/local/bin`, where GoloScript's own installer writes, and its absence is reported on the status bar with the address of the release page that fixes it. | |
| 82 | + | |
| 83 | +## The language server is the interpreter | |
| 84 | + | |
| 85 | +There is no separate `golo-lsp` binary to find, version and keep in step with the interpreter: `golo lsp` puts the same binary that runs a script into language-server mode, reusing its lexer, parser and syntax tree. A machine that can run Golo can complete Golo. | |
| 86 | + | |
| 87 | +The consequence worth knowing is what that server does *not* answer. It advertises completion, hover, definition, the file's symbols, references, implementations and project-wide symbols, and it publishes diagnostics unasked; it does not advertise type definition — and until GoloScript v0.2.0 it advertised none of the last three either. The editor's **Code** menu still lists all of them, because the menu is the library's and the same for every editor of the family. The alternative — greying out what this server cannot do — was turned down in favour of saying so in the documentation and pinning it with a test, so that a future `golo` gaining one of those answers is noticed rather than silently ignored — which is exactly how the three it gained were noticed. The item that is left reports nothing found, which is the truth, in the question's own words. | |
| 88 | + | |
| 89 | +## There is no project marker | |
| 90 | + | |
| 91 | +Turbo Go walks up from the file to `go.mod`, Turbo Rust to `Cargo.toml`, and hands the language server that directory as its root. Turbo Golo walks nowhere: Golo has no manifest and no build file — a script is a file, and a program is a directory of them — so the server is started in the directory of the file being edited, or in the working directory when there is no file. | |
| 92 | + | |
| 93 | +A marker was considered and rejected because it would make the walk look like part of a rule when there is no rule. `golo lsp` answers about the file it is given and resolves imports from the modules embedded in the binary, never from disk, so the directory it starts in changes nothing about its answers; the only honest root is the one that needs no explaining. | |
| 94 | + | |
| 95 | +## Saving is atomic, and byte-faithful | |
| 96 | + | |
| 97 | +A save writes to a temporary file in the same directory and renames it over the target, preserving the original's permissions. An interrupted save cannot leave a half-written source file. | |
| 98 | + | |
| 99 | +Separately, the line endings a file was read with and its trailing newline — or lack of one — are remembered, so opening and saving an untouched file reproduces it byte for byte. An editor that silently normalises line endings turns a one-line change into a whole-file diff. | |
| 100 | + | |
| 101 | +## The clipboard is the editor's own | |
| 102 | + | |
| 103 | +A terminal program cannot read the host clipboard portably. Rather than pretend, Turbo Golo shares one clipboard between its own windows, which is what Turbo C did. | |
| 104 | + | |
| 105 | +## The version is a property of the build, not of the source | |
| 106 | + | |
| 107 | +The version used to be `const Version = "0.1.0"` in the editor's own source. It was accurate the day it was written and wrong for the fourteen commits after it, because nothing in the process of committing, tagging or installing touches a Go constant. An About box is where somebody looks when they are about to report a bug; a number there that names a release the binary is not is worse than no number, because it is believed. | |
| 108 | + | |
| 109 | +So the number is taken from the build. The linker stamps `git describe --tags --dirty` into turbo-core's `version` package from the Makefile and from the installer, which is what makes `make install` produce an editor that names the commit it came from. When nothing stamped it, the binary asks `runtime/debug.ReadBuildInfo()`, which covers the one path that cannot be stamped: `go install rickub.com/turbo-editors/turbo-golo@v0.1.0`, where there is no Makefile in the picture and the Go tool knows the module version. Only when both are silent does it say `unknown` — deliberately not a number, because the whole failure being designed against is a plausible-looking version nobody set. | |
| 110 | + | |
| 111 | +Two things the build system cannot do explain the rest of the design. **It does not read git tags**, so a plain `go build .` can never report `0.1.0-14-g88a4c38` however clever the code is; it reports `devel` plus the commit, and the documentation says so rather than implying that every build is equal. And what it *does* report for such a build is a **pseudo-version** — `v0.1.1-0.20260914120000-0123456789ab`, say — which is shown as `devel` instead, because its `0.1.1` is a patch release that does not exist and would be read as one. | |
| 112 | + | |
| 113 | +`vcs.time` is deliberately unused. It is the commit's timestamp, and every binary is linked later than the commit it was built from, so labelling it "Built" would be false on all of them. A build date is shown only when a build actually stamped one, which is the same rule the About box follows throughout: **a fact nobody recorded gets no line**, rather than an empty one that reads as a failure to fill it in. | |
| 114 | + | |
| 115 | +Rejected: a `make release` target that tags, builds and pushes. Releasing is three git commands, and wrapping them hides which of them failed; the version stamping is the part that could not be done by hand reliably, and that is the part that was automated. The numbered release scripts that exist do each step separately, and stop at the first one that fails. | |
| 116 | + | |
| 117 | +## How it relates to the rest | |
| 118 | + | |
| 119 | +- What the packages are and how they fit: [Architecture](architecture.md) | |
| 120 | +- How the colouring and the completion work: [Colouring and completion](colouring-and-completion.md) | |
| 121 | +- What `golo lsp` answers, exactly: [Golo tools](golo-tools.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,121 @@ | |||
| 1 | +# Design decisions — explanation | ||
| 2 | + | ||
| 3 | +## What is this about? | ||
| 4 | + | ||
| 5 | +The choices that shaped Turbo Golo, what the alternatives were, and why they were turned down. Most of them were made once, in [turbo-core](https://rickub.com/turbo-editors/turbo-core), and every editor of the family inherits them; the ones about the language server and the toolchain are this editor's own. This is the page to read before changing something that looks arbitrary. | ||
| 6 | + | ||
| 7 | +## Two dependencies, and no more | ||
| 8 | + | ||
| 9 | +Turbo Golo depends on turbo-core, and through it on `tcell/v2` and `BurntSushi/toml`. Everything else is the standard library — including the tokeniser, the JSON-RPC client, the LSP framing and the file handling. | ||
| 10 | + | ||
| 11 | +**What was rejected.** `go.lsp.dev/jsonrpc2` would have saved a few hundred lines of the library's `lsp` package. `rivo/tview` would have saved rather more of its `ui`. A syntax-highlighting library would have brought fifty languages instead of one. | ||
| 12 | + | ||
| 13 | +**Why.** An editor is a program you keep for years and change often. Every dependency is a piece of it you cannot change, cannot fully test, and have to track. The protocol is simple enough to write down, and writing it down put the whole conversation somewhere a reader can follow. Three hundred lines you understand beat three hundred you inherit. | ||
| 14 | + | ||
| 15 | +The exception proves the rule: `tcell` is not a convenience, it is the terminal-compatibility database, and reimplementing that would be neither small nor honest work. | ||
| 16 | + | ||
| 17 | +## The widget framework is hand-written | ||
| 18 | + | ||
| 19 | +`tview` has widgets. `bubbletea` has an architecture. Neither has what Turbo Vision had: overlapping movable windows with shadows, a menu bar with hot keys, and modal dialogs, all drawn with box characters in sixteen colours. | ||
| 20 | + | ||
| 21 | +The Elm-style architecture that `bubbletea` uses re-renders the whole view on every message. That model is excellent for a form and awkward for a full-screen editor with windows stacked on top of each other and a cursor that has to be in one exact cell. | ||
| 22 | + | ||
| 23 | +Writing the framework cost roughly fifteen hundred lines. In exchange the editor looks like Turbo C rather than like a modern TUI wearing a blue background, and every drawing decision is one file away. | ||
| 24 | + | ||
| 25 | +## Bounds are absolute screen coordinates | ||
| 26 | + | ||
| 27 | +Every widget's `Bounds()` is where it really is on the terminal, not where it is relative to its parent. Hit-testing a mouse click is then a plain rectangle test, and no event ever needs translating on its way down. | ||
| 28 | + | ||
| 29 | +**The cost** is that containers place their children in screen space. **The alternative** — relative coordinates with a translation at each hop — moves the arithmetic from layout into event handling, where it is done far more often and is far easier to get wrong. Clipping still composes correctly because a painter intersects its parent's clip, so a child with wrong arithmetic draws nothing rather than drawing over its neighbours. | ||
| 30 | + | ||
| 31 | +## Every change goes through one function | ||
| 32 | + | ||
| 33 | +`buffer.ReplaceRange` is the only place the text is modified. Insert, backspace, delete, indent, paste and undo all funnel through it, and it is the only place the undo history, the modified flag, the revision counter and the cursor are maintained. | ||
| 34 | + | ||
| 35 | +The alternative — each operation maintaining its own bookkeeping — is how undo bugs are born. There is exactly one thing to get right, and it is tested directly. | ||
| 36 | + | ||
| 37 | +## Windows follow the terminal, they do not scale with it | ||
| 38 | + | ||
| 39 | +A window has a **grow mode**, which names the desktop edges it follows. A document window follows the right and bottom edges: its top-left corner stays where it is, and its far corner moves by exactly as much as the terminal's did. A window that filled the terminal therefore still fills it, and one you had cascaded keeps its offset. | ||
| 40 | + | ||
| 41 | +**The alternative was proportional scaling** — multiply every window's rectangle by the ratio of the old and new sizes. It was rejected because it moves windows the user deliberately placed, and because rounding makes it lossy: shrink and grow again and nothing is where it was. Turbo Vision used grow modes, and they are still the right answer. | ||
| 42 | + | ||
| 43 | +Whatever its grow mode, a window is then held to the desktop's own size. One larger than the desktop that holds it has parts nobody can reach. | ||
| 44 | + | ||
| 45 | +## A window's boxes say what they will do, not what the window is | ||
| 46 | + | ||
| 47 | +The frame carries two boxes: `[x]` at the left closes the window, `[■]` at the right fills the desktop. | ||
| 48 | + | ||
| 49 | +The close box used to be `[■]` — Turbo Vision's own — and it had to move. Two boxes on one frame need to be told apart at a glance, and a filled block reads as "fill the screen" far more readily than as "close". `[x]` is what a close button has meant for thirty years; the block went to the job it actually looks like. | ||
| 50 | + | ||
| 51 | +The maximise box **changes with the window's state**: `[■]` while there is room to grow, `[▬]` once the window fills the desktop. The alternative was a fixed symbol, and it makes the button ambiguous exactly when you need it — you can see that the window is large, but not whether pressing the box will make it larger still or put it back. A control that shows its *current state* leaves you to work out the action; one that shows its *action* does not. | ||
| 52 | + | ||
| 53 | +A window with nowhere to maximise into shows **no box at all**, rather than one that does nothing. Only the desktop knows what area a window would fill, so a window that is not on one has nothing to offer. | ||
| 54 | + | ||
| 55 | +**Window ▸ Maximise is the same toggle**, not a one-way action. A menu item and a button that disagreed about what "maximise" means would be a bug people reported rather than a subtlety they appreciated. | ||
| 56 | + | ||
| 57 | +## Undo merges runs of typing | ||
| 58 | + | ||
| 59 | +Typing `function` and pressing Ctrl-Z removes all eight letters. So does a run of backspaces. Moving the cursor ends the run, and typing never merges with deleting. | ||
| 60 | + | ||
| 61 | +Character-by-character undo is what a naive implementation gives you, and it is what Turbo C itself did. It is also what nobody wants any more. | ||
| 62 | + | ||
| 63 | +## Themes are TOML, with two kinds of inheritance | ||
| 64 | + | ||
| 65 | +**Between files**, `inherits` takes the parent's resolved styles as the starting point. A theme of your own can therefore be five lines. | ||
| 66 | + | ||
| 67 | +**Between keys**, along the dots: `syntax.keyword` falls back to `syntax`, and `syntax` to `default`. This happens twice — once at parse time, so an entry setting only `fg` inherits its `bg`, and once at lookup time, so a theme that never mentions `syntax.keyword` still colours keywords. | ||
| 68 | + | ||
| 69 | +That second one is what makes a partial theme a usable theme, and it is why there is no such thing as a theme that leaves half the screen unpainted. | ||
| 70 | + | ||
| 71 | +**Why TOML rather than JSON.** Comments. A theme is a file people edit by hand and annotate. | ||
| 72 | + | ||
| 73 | +**An unknown colour is an error**, not a silent fallback to the terminal default. A typo that quietly repaints half the screen is much harder to find than one that says so at load. | ||
| 74 | + | ||
| 75 | +## The language server is optional by construction | ||
| 76 | + | ||
| 77 | +`app.Language` wraps the whole `golo lsp` conversation, and when there is no server every method is a no-op rather than an error. Nothing else in the editor asks whether a language server exists. | ||
| 78 | + | ||
| 79 | +The alternative — checking for `nil` at every call site — is that many chances to forget. Here, forgetting is impossible: there is nothing to check. | ||
| 80 | + | ||
| 81 | +This is why `golo` is not bundled, not downloaded, and not required. It is looked for on `PATH` and then in `/usr/local/bin`, where GoloScript's own installer writes, and its absence is reported on the status bar with the address of the release page that fixes it. | ||
| 82 | + | ||
| 83 | +## The language server is the interpreter | ||
| 84 | + | ||
| 85 | +There is no separate `golo-lsp` binary to find, version and keep in step with the interpreter: `golo lsp` puts the same binary that runs a script into language-server mode, reusing its lexer, parser and syntax tree. A machine that can run Golo can complete Golo. | ||
| 86 | + | ||
| 87 | +The consequence worth knowing is what that server does *not* answer. It advertises completion, hover, definition, the file's symbols, references, implementations and project-wide symbols, and it publishes diagnostics unasked; it does not advertise type definition — and until GoloScript v0.2.0 it advertised none of the last three either. The editor's **Code** menu still lists all of them, because the menu is the library's and the same for every editor of the family. The alternative — greying out what this server cannot do — was turned down in favour of saying so in the documentation and pinning it with a test, so that a future `golo` gaining one of those answers is noticed rather than silently ignored — which is exactly how the three it gained were noticed. The item that is left reports nothing found, which is the truth, in the question's own words. | ||
| 88 | + | ||
| 89 | +## There is no project marker | ||
| 90 | + | ||
| 91 | +Turbo Go walks up from the file to `go.mod`, Turbo Rust to `Cargo.toml`, and hands the language server that directory as its root. Turbo Golo walks nowhere: Golo has no manifest and no build file — a script is a file, and a program is a directory of them — so the server is started in the directory of the file being edited, or in the working directory when there is no file. | ||
| 92 | + | ||
| 93 | +A marker was considered and rejected because it would make the walk look like part of a rule when there is no rule. `golo lsp` answers about the file it is given and resolves imports from the modules embedded in the binary, never from disk, so the directory it starts in changes nothing about its answers; the only honest root is the one that needs no explaining. | ||
| 94 | + | ||
| 95 | +## Saving is atomic, and byte-faithful | ||
| 96 | + | ||
| 97 | +A save writes to a temporary file in the same directory and renames it over the target, preserving the original's permissions. An interrupted save cannot leave a half-written source file. | ||
| 98 | + | ||
| 99 | +Separately, the line endings a file was read with and its trailing newline — or lack of one — are remembered, so opening and saving an untouched file reproduces it byte for byte. An editor that silently normalises line endings turns a one-line change into a whole-file diff. | ||
| 100 | + | ||
| 101 | +## The clipboard is the editor's own | ||
| 102 | + | ||
| 103 | +A terminal program cannot read the host clipboard portably. Rather than pretend, Turbo Golo shares one clipboard between its own windows, which is what Turbo C did. | ||
| 104 | + | ||
| 105 | +## The version is a property of the build, not of the source | ||
| 106 | + | ||
| 107 | +The version used to be `const Version = "0.1.0"` in the editor's own source. It was accurate the day it was written and wrong for the fourteen commits after it, because nothing in the process of committing, tagging or installing touches a Go constant. An About box is where somebody looks when they are about to report a bug; a number there that names a release the binary is not is worse than no number, because it is believed. | ||
| 108 | + | ||
| 109 | +So the number is taken from the build. The linker stamps `git describe --tags --dirty` into turbo-core's `version` package from the Makefile and from the installer, which is what makes `make install` produce an editor that names the commit it came from. When nothing stamped it, the binary asks `runtime/debug.ReadBuildInfo()`, which covers the one path that cannot be stamped: `go install rickub.com/turbo-editors/turbo-golo@v0.1.0`, where there is no Makefile in the picture and the Go tool knows the module version. Only when both are silent does it say `unknown` — deliberately not a number, because the whole failure being designed against is a plausible-looking version nobody set. | ||
| 110 | + | ||
| 111 | +Two things the build system cannot do explain the rest of the design. **It does not read git tags**, so a plain `go build .` can never report `0.1.0-14-g88a4c38` however clever the code is; it reports `devel` plus the commit, and the documentation says so rather than implying that every build is equal. And what it *does* report for such a build is a **pseudo-version** — `v0.1.1-0.20260914120000-0123456789ab`, say — which is shown as `devel` instead, because its `0.1.1` is a patch release that does not exist and would be read as one. | ||
| 112 | + | ||
| 113 | +`vcs.time` is deliberately unused. It is the commit's timestamp, and every binary is linked later than the commit it was built from, so labelling it "Built" would be false on all of them. A build date is shown only when a build actually stamped one, which is the same rule the About box follows throughout: **a fact nobody recorded gets no line**, rather than an empty one that reads as a failure to fill it in. | ||
| 114 | + | ||
| 115 | +Rejected: a `make release` target that tags, builds and pushes. Releasing is three git commands, and wrapping them hides which of them failed; the version stamping is the part that could not be done by hand reliably, and that is the part that was automated. The numbered release scripts that exist do each step separately, and stop at the first one that fails. | ||
| 116 | + | ||
| 117 | +## How it relates to the rest | ||
| 118 | + | ||
| 119 | +- What the packages are and how they fit: [Architecture](architecture.md) | ||
| 120 | +- How the colouring and the completion work: [Colouring and completion](colouring-and-completion.md) | ||
| 121 | +- What `golo lsp` answers, exactly: [Golo tools](golo-tools.md) | ||
added
docs/en/explanation/golo-tools.md +119 -0 | new file mode 100644 | ||
| @@ -0,0 +1,119 @@ | ||
| 1 | +# Golo tools — explanation | |
| 2 | + | |
| 3 | +## What is this about? | |
| 4 | + | |
| 5 | +A **Golo** menu whose commands come from a TOML file, each run where the tool asked — a popup, a terminal window, or an editing window — and the open files re-read afterwards. This page is about why each of those is the way it is. | |
| 6 | + | |
| 7 | +## Why the output has three places to go, and a popup by default | |
| 8 | + | |
| 9 | +The first version of this mechanism, in Turbo Go, put every command in a terminal window, and it was the wrong default for most of them. | |
| 10 | + | |
| 11 | +A terminal is the right answer when the program is *interactive or long*: `golo main.golo` on a script that reads the keyboard with `readln` has to be answerable, and a script that serves HTTP with `httpServe` has to be interruptible with `Ctrl-C`. Neither is true of `golo --test`, which prints its report and ends. Giving that a whole window — one you then have to close, on a desktop where windows overlap and are numbered — is more ceremony than the result deserves. | |
| 12 | + | |
| 13 | +A popup is the right answer for a command you run, read and dismiss. It is modal, which is a real cost and is named in the [how-to](../how-to/run-golo-commands.md): a `gogolo build` you did not expect to be slow — it runs the Go compiler — holds the editor until it finishes or you press Escape. That cost was accepted on purpose, because the alternative — a dialog appearing unbidden three seconds later — swallows whatever was being typed at the moment it arrives. | |
| 14 | + | |
| 15 | +So the popup **opens immediately and fills in**. You see progress, nothing surprises you, and Escape both closes it and stops the command, which is the only way to interrupt something whose output is not in a terminal. | |
| 16 | + | |
| 17 | +An editing window is the right answer for output you are going to work through: a long test report, or the Go source `gogolo transpile` prints. It is an ordinary buffer, so `Ctrl-F` searches it and `Save as` keeps it. It is filled once the command has ended rather than as it goes, because a buffer growing under the cursor while you search it is the opposite of what that mode is for. | |
| 18 | + | |
| 19 | +None of those three is right for everything, which is why `output` is in the file rather than in the code. `Run` is the worked example: it is the first command in the starter file, it says `terminal`, and the comment beside it says why. So do `Debug` — the step debugger reads the keyboard — and `REPL`, which is nothing but a keyboard. | |
| 20 | + | |
| 21 | +## Why Run comes first | |
| 22 | + | |
| 23 | +In Turbo MoonBit's starter file the first tool is `moon check`, because for a compiled language "is this sound?" is the question asked most often and the one that produces nothing. Golo is a scripting language, and the question asked most often is "what does it print?". So the first line of the Golo menu runs the file, and the second runs the tests. | |
| 24 | + | |
| 25 | +## Why a terminal window is still there | |
| 26 | + | |
| 27 | +The editor already had one — a real pseudo-terminal with a VT emulator, built for the `F8` windows — so `output = "terminal"` costs one field on its options and buys colours, paging, `Ctrl-C`, keyboard input and scrollback for nothing, because they are the same mechanisms every other terminal uses. GoloScript's own output uses those colours: `golo --test` draws green ticks, and the `uiPrint` family of builtins draws whatever the script asked for. | |
| 28 | + | |
| 29 | +The window stays after the command exits, which is the point: the output is what you asked for, and a window that vanished with it would be useless. | |
| 30 | + | |
| 31 | +## Why the exit code is always in the title | |
| 32 | + | |
| 33 | +A script that ends with nothing printed, or `golo --test` over a directory with no test files, prints nothing at all. A popup with an empty body and a neutral title is indistinguishable from one whose command has not started, and the reader is left guessing at the one thing they wanted to know. | |
| 34 | + | |
| 35 | +So the title carries the verdict — `— ok` or `— exit 1` — and an empty body says `(no output)` once the command has ended. While it is still running the body stays blank, because "(no output)" is a verdict and a running command has not reached one. | |
| 36 | + | |
| 37 | +## Why the commands are in a file | |
| 38 | + | |
| 39 | +Eight commands hardwired into the editor would have answered the request. They would also have been wrong within a week. | |
| 40 | + | |
| 41 | +Every command in the starter file goes through one of GoloScript's three binaries — `golo`, `gogolo` or `wagolo` — so none of them needs anything set up beyond the toolchain itself. That is a defensible default and it is nobody's universal answer. A project with one entry point wants `golo main.golo` without being asked which script. One that ships as a native binary wants `gogolo build -o bin/app app.golo` with the output fixed. One that targets the browser wants `wagolo build -target=js` and never `wasi`. One that runs under Docker wants `docker run … k33g/gololang`. None of that is knowable from here, and all of it is one line in a file. | |
| 42 | + | |
| 43 | +So the eight are **defaults, not code**: they are the contents of the starter file that **Golo ▸ Create tools file** writes, and changing one is editing a file rather than rebuilding an editor. The file is read every time the menu opens, for the same reason the Snippets menu is: an edit should take effect at once, and the file is often open in the window behind the menu. | |
| 44 | + | |
| 45 | +Commands go to `sh -c` — `cmd.exe /S /C` on Windows — rather than being split into an argv here. The file is the user's own, so pipes, globs and `&&` are features rather than hazards, and one entry can be `golo --test && gogolo build -o app main.golo`. Splitting an argv would mean inventing quoting rules for a string somebody wrote by hand. | |
| 46 | + | |
| 47 | +## Why there is no user-level tools file | |
| 48 | + | |
| 49 | +Snippets are read from two files — yours and the project's — because your snippets are your habits and should follow you between projects. | |
| 50 | + | |
| 51 | +Tools are not like that. They belong to a project's own toolchain: a global tools file would offer `golo --test` in a repository that has never heard of Golo, and a project that only ever interprets its scripts would get `wagolo build` in its menu with a TinyGo it never installed. The file is per-project, and that is the whole of the rule. | |
| 52 | + | |
| 53 | +## Why a tool may name its own menu | |
| 54 | + | |
| 55 | +A menu called **Golo** holding `docker compose up` is a lie about what the menu is. The first tools file anybody writes outgrows Golo, because a project's commands are not all about the language it is written in: containers, databases, deploys, a `Makefile` target somebody added in 2019. | |
| 56 | + | |
| 57 | +Two shapes were considered. A **fixed second menu** called Tools — everything Golo in Golo, everything else in Tools — is one key in the format and no naming problem at all, but it only moves the lie: a Tools menu holding `docker compose up`, `psql`, and a deploy script is just as undifferentiated, and the moment there are ten entries nobody can find one. And a **second file**, `menus.toml`, keeps the tools file simple at the cost of two files that have to agree about which tools exist. | |
| 58 | + | |
| 59 | +So the menu is a **free-form name on the tool**, in the one file: `menu = "Docker"`. A name nothing else uses creates the menu; leaving the key out means Golo. There is no list of allowed names, because a list would be a list of somebody else's projects. | |
| 60 | + | |
| 61 | +Golo itself stays fixed on the bar rather than becoming just another name from the file. **Golo ▸ Create tools file** has to be reachable in a project that has no tools file at all — which is exactly the project that needs it — and a menu that only exists once the file exists cannot offer to write the file. | |
| 62 | + | |
| 63 | +## Why the hot key is not the file's to choose | |
| 64 | + | |
| 65 | +The author of a tools file cannot know which letters are free. They can see `File`, `Edit`, `Search`, `Run`, `Code`, `Options`, `Window`, `Snippets`, `Golo` and `Help` on the bar, but only by counting the underlines, and a project shared between people would then depend on nobody adding a menu that collides. | |
| 66 | + | |
| 67 | +Collisions here are **silent**, which is what makes them worth designing against. The bar answers the first menu whose hot key matches; a second menu claiming the same letter is not an error and draws normally — it simply never opens. That trap has already been sprung once in this family: `Snippets` and `Search` both wanted `S`, `Snippets` was the unreachable one, and every test passed. The fix then was to move Snippets to `N` by hand. Letting a file name menus makes that a permanent hazard rather than a one-off mistake, so the assignment is done by the editor: the first letter of the name nothing else claims. | |
| 68 | + | |
| 69 | +Tildes written into the name are honoured **when the letter is free**, and quietly overridden when it is not. Refusing the file instead was the alternative, and it is worse: the clash depends on which menus exist, so a tools file that worked would break the day an editor release added a menu. Between a menu on a letter you did not ask for and a menu you cannot open, the first is the smaller loss. | |
| 70 | + | |
| 71 | +When every letter of a name is taken, the menu gets no hot key at all. `F10`, the arrow keys and the mouse still reach it, and the alternative — reaching for a letter that is not in the name — would put an underline under nothing. | |
| 72 | + | |
| 73 | +## Why the bar is rebuilt from a stat | |
| 74 | + | |
| 75 | +`Menu.OnOpen` refills a menu's items just before it drops down, which is how the Golo and Snippets menus follow their files without a restart. It cannot help here: the *set* of menus is part of the bar, not part of any one menu, and adding `menu = "Docker"` to the file should put Docker on the bar. | |
| 76 | + | |
| 77 | +Reading and parsing the file on every turn of the event loop would do it, and would also be work done for nothing on every keystroke of a file nobody has edited. So the bar carries the size and modification time of the tools file it was built from, and one `stat` per turn decides whether to rebuild. Editing the file in the window in front of you, saving it, and watching the bar change is the case this is for. | |
| 78 | + | |
| 79 | +## Why open files are re-read, and only some of them | |
| 80 | + | |
| 81 | +Golo has no formatter, so no starter command rewrites the file in front of you — but `golo new` writes a file into the directory, `gogolo build -keep-go` leaves a `.go` beside the script, and your own tools may do anything at all. Without anything further, the editor would sit on a stale copy of a file another command changed, and the next `F2` would write your copy back over the command's work. | |
| 82 | + | |
| 83 | +So when a command finishes, the editor re-reads every open file. The interesting part is which ones it refuses to touch. | |
| 84 | + | |
| 85 | +**A file with unsaved changes is left alone**, and the status bar says how many were skipped. Reloading it would throw away work the user has not saved, which no amount of convenience justifies. And the conflict is genuine: the command and the unsaved edit disagree about what the file should say, and the editor is not in a position to decide. Naming it and stopping is the honest outcome. | |
| 86 | + | |
| 87 | +Two smaller decisions inside that: | |
| 88 | + | |
| 89 | +- **The cursor stays where it was**, clamped into whatever the file now holds. | |
| 90 | +- **The undo history is discarded.** Undoing back past a reload would restore text the file no longer has, which is worse than not being able to undo at all. | |
| 91 | + | |
| 92 | +## Why the reload happens on the event loop | |
| 93 | + | |
| 94 | +The command's exit is noticed on the goroutine reading the terminal, which may not touch a buffer or the desktop. So it sets a flag, and the reload runs at the top of the next turn of the event loop. | |
| 95 | + | |
| 96 | +This is the fourth thing in the library built that way — the language-server announcement, the terminal redraws, the autosave deadline, and now this. The rule they share is worth stating once more: **the wake-up may be lost, so the state must not be.** `PostEvent` drops what does not fit in its queue, so anything that depends on a message arriving is a bug waiting for a busy moment. A flag the loop checks for itself cannot go missing. | |
| 97 | + | |
| 98 | +## Why a command can ask for a value, and why it asks in double braces | |
| 99 | + | |
| 100 | +`golo` needs a script. `golo new` needs a module name and a file name. `gogolo build` needs a script and an output path. `wagolo build` needs a target too. None of those can live in the tools file as a fixed string, because the answer is different every time — and a tool that cannot ask is a tool that has to be edited before each use, which is not a tool. | |
| 101 | + | |
| 102 | +So a `{{label}}` in a command is a value the editor asks for first, in a box titled after the tool. **Six of the eight Golo commands use it**, which is deliberate: a feature demonstrated in the file everybody gets is a feature people find, and one described only in a comment is not. That is more placeholders than any sibling's starter file carries, and the reason is Golo's: with no manifest there is no `moon run` that knows what to run, so every command that touches a file has to be told which one. | |
| 103 | + | |
| 104 | +**Single braces were the obvious spelling and are wrong.** `awk '{print $1}'` and `find . -exec rm {} +` are ordinary things to put in a tools file, and reading the first as a placeholder turns a working command into a box asking for "print $1". Double braces collide with almost nothing. | |
| 105 | + | |
| 106 | +**The value is quoted by default**, because the alternative fails silently. A path with a space in it, substituted raw, becomes two arguments and the command reports something about a file that does not exist. Quoting makes that case work and makes the other case — "put these three flags on the end" — impossible, so `...` inside the braces asks for the value verbatim. Two behaviours, both documented, rather than one that is wrong half the time. | |
| 107 | + | |
| 108 | +**Nothing is remembered on disk.** The box starts from what was typed last time, for the session. Writing it into the project's own directory was considered and rejected: that directory holds what the project decided, and the script somebody ran while chasing one bug is not that. | |
| 109 | + | |
| 110 | +**A file that cannot be parsed is refused when it is read**, not when the tool is chosen. An unclosed `{{` reaching the shell is a command failing with braces in it, which names neither the tool nor the file; refusing at load names both. That is the same rule an unknown `output` value already follows. | |
| 111 | + | |
| 112 | +**The dialog is refused when it will not fit.** A tool asking for more values than the terminal has rows would give a box whose OK button is below the bottom of the screen — answerable only by Escape, which cancels. Saying "this asks for twelve values and nine fit" is worse than nothing only if you would rather find out by trying. | |
| 113 | + | |
| 114 | +## How it relates to the rest | |
| 115 | + | |
| 116 | +- Every key of the file and every rule: [Golo tools reference](../reference/golo-tools.md) | |
| 117 | +- Using it: [How to run Golo commands from the editor](../how-to/run-golo-commands.md) | |
| 118 | +- The windows `output = "terminal"` uses, and why they are real terminals: [Terminal windows](terminal-windows.md) | |
| 119 | +- The other menu built from a file: [Snippets](snippets.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,119 @@ | |||
| 1 | +# Golo tools — explanation | ||
| 2 | + | ||
| 3 | +## What is this about? | ||
| 4 | + | ||
| 5 | +A **Golo** menu whose commands come from a TOML file, each run where the tool asked — a popup, a terminal window, or an editing window — and the open files re-read afterwards. This page is about why each of those is the way it is. | ||
| 6 | + | ||
| 7 | +## Why the output has three places to go, and a popup by default | ||
| 8 | + | ||
| 9 | +The first version of this mechanism, in Turbo Go, put every command in a terminal window, and it was the wrong default for most of them. | ||
| 10 | + | ||
| 11 | +A terminal is the right answer when the program is *interactive or long*: `golo main.golo` on a script that reads the keyboard with `readln` has to be answerable, and a script that serves HTTP with `httpServe` has to be interruptible with `Ctrl-C`. Neither is true of `golo --test`, which prints its report and ends. Giving that a whole window — one you then have to close, on a desktop where windows overlap and are numbered — is more ceremony than the result deserves. | ||
| 12 | + | ||
| 13 | +A popup is the right answer for a command you run, read and dismiss. It is modal, which is a real cost and is named in the [how-to](../how-to/run-golo-commands.md): a `gogolo build` you did not expect to be slow — it runs the Go compiler — holds the editor until it finishes or you press Escape. That cost was accepted on purpose, because the alternative — a dialog appearing unbidden three seconds later — swallows whatever was being typed at the moment it arrives. | ||
| 14 | + | ||
| 15 | +So the popup **opens immediately and fills in**. You see progress, nothing surprises you, and Escape both closes it and stops the command, which is the only way to interrupt something whose output is not in a terminal. | ||
| 16 | + | ||
| 17 | +An editing window is the right answer for output you are going to work through: a long test report, or the Go source `gogolo transpile` prints. It is an ordinary buffer, so `Ctrl-F` searches it and `Save as` keeps it. It is filled once the command has ended rather than as it goes, because a buffer growing under the cursor while you search it is the opposite of what that mode is for. | ||
| 18 | + | ||
| 19 | +None of those three is right for everything, which is why `output` is in the file rather than in the code. `Run` is the worked example: it is the first command in the starter file, it says `terminal`, and the comment beside it says why. So do `Debug` — the step debugger reads the keyboard — and `REPL`, which is nothing but a keyboard. | ||
| 20 | + | ||
| 21 | +## Why Run comes first | ||
| 22 | + | ||
| 23 | +In Turbo MoonBit's starter file the first tool is `moon check`, because for a compiled language "is this sound?" is the question asked most often and the one that produces nothing. Golo is a scripting language, and the question asked most often is "what does it print?". So the first line of the Golo menu runs the file, and the second runs the tests. | ||
| 24 | + | ||
| 25 | +## Why a terminal window is still there | ||
| 26 | + | ||
| 27 | +The editor already had one — a real pseudo-terminal with a VT emulator, built for the `F8` windows — so `output = "terminal"` costs one field on its options and buys colours, paging, `Ctrl-C`, keyboard input and scrollback for nothing, because they are the same mechanisms every other terminal uses. GoloScript's own output uses those colours: `golo --test` draws green ticks, and the `uiPrint` family of builtins draws whatever the script asked for. | ||
| 28 | + | ||
| 29 | +The window stays after the command exits, which is the point: the output is what you asked for, and a window that vanished with it would be useless. | ||
| 30 | + | ||
| 31 | +## Why the exit code is always in the title | ||
| 32 | + | ||
| 33 | +A script that ends with nothing printed, or `golo --test` over a directory with no test files, prints nothing at all. A popup with an empty body and a neutral title is indistinguishable from one whose command has not started, and the reader is left guessing at the one thing they wanted to know. | ||
| 34 | + | ||
| 35 | +So the title carries the verdict — `— ok` or `— exit 1` — and an empty body says `(no output)` once the command has ended. While it is still running the body stays blank, because "(no output)" is a verdict and a running command has not reached one. | ||
| 36 | + | ||
| 37 | +## Why the commands are in a file | ||
| 38 | + | ||
| 39 | +Eight commands hardwired into the editor would have answered the request. They would also have been wrong within a week. | ||
| 40 | + | ||
| 41 | +Every command in the starter file goes through one of GoloScript's three binaries — `golo`, `gogolo` or `wagolo` — so none of them needs anything set up beyond the toolchain itself. That is a defensible default and it is nobody's universal answer. A project with one entry point wants `golo main.golo` without being asked which script. One that ships as a native binary wants `gogolo build -o bin/app app.golo` with the output fixed. One that targets the browser wants `wagolo build -target=js` and never `wasi`. One that runs under Docker wants `docker run … k33g/gololang`. None of that is knowable from here, and all of it is one line in a file. | ||
| 42 | + | ||
| 43 | +So the eight are **defaults, not code**: they are the contents of the starter file that **Golo ▸ Create tools file** writes, and changing one is editing a file rather than rebuilding an editor. The file is read every time the menu opens, for the same reason the Snippets menu is: an edit should take effect at once, and the file is often open in the window behind the menu. | ||
| 44 | + | ||
| 45 | +Commands go to `sh -c` — `cmd.exe /S /C` on Windows — rather than being split into an argv here. The file is the user's own, so pipes, globs and `&&` are features rather than hazards, and one entry can be `golo --test && gogolo build -o app main.golo`. Splitting an argv would mean inventing quoting rules for a string somebody wrote by hand. | ||
| 46 | + | ||
| 47 | +## Why there is no user-level tools file | ||
| 48 | + | ||
| 49 | +Snippets are read from two files — yours and the project's — because your snippets are your habits and should follow you between projects. | ||
| 50 | + | ||
| 51 | +Tools are not like that. They belong to a project's own toolchain: a global tools file would offer `golo --test` in a repository that has never heard of Golo, and a project that only ever interprets its scripts would get `wagolo build` in its menu with a TinyGo it never installed. The file is per-project, and that is the whole of the rule. | ||
| 52 | + | ||
| 53 | +## Why a tool may name its own menu | ||
| 54 | + | ||
| 55 | +A menu called **Golo** holding `docker compose up` is a lie about what the menu is. The first tools file anybody writes outgrows Golo, because a project's commands are not all about the language it is written in: containers, databases, deploys, a `Makefile` target somebody added in 2019. | ||
| 56 | + | ||
| 57 | +Two shapes were considered. A **fixed second menu** called Tools — everything Golo in Golo, everything else in Tools — is one key in the format and no naming problem at all, but it only moves the lie: a Tools menu holding `docker compose up`, `psql`, and a deploy script is just as undifferentiated, and the moment there are ten entries nobody can find one. And a **second file**, `menus.toml`, keeps the tools file simple at the cost of two files that have to agree about which tools exist. | ||
| 58 | + | ||
| 59 | +So the menu is a **free-form name on the tool**, in the one file: `menu = "Docker"`. A name nothing else uses creates the menu; leaving the key out means Golo. There is no list of allowed names, because a list would be a list of somebody else's projects. | ||
| 60 | + | ||
| 61 | +Golo itself stays fixed on the bar rather than becoming just another name from the file. **Golo ▸ Create tools file** has to be reachable in a project that has no tools file at all — which is exactly the project that needs it — and a menu that only exists once the file exists cannot offer to write the file. | ||
| 62 | + | ||
| 63 | +## Why the hot key is not the file's to choose | ||
| 64 | + | ||
| 65 | +The author of a tools file cannot know which letters are free. They can see `File`, `Edit`, `Search`, `Run`, `Code`, `Options`, `Window`, `Snippets`, `Golo` and `Help` on the bar, but only by counting the underlines, and a project shared between people would then depend on nobody adding a menu that collides. | ||
| 66 | + | ||
| 67 | +Collisions here are **silent**, which is what makes them worth designing against. The bar answers the first menu whose hot key matches; a second menu claiming the same letter is not an error and draws normally — it simply never opens. That trap has already been sprung once in this family: `Snippets` and `Search` both wanted `S`, `Snippets` was the unreachable one, and every test passed. The fix then was to move Snippets to `N` by hand. Letting a file name menus makes that a permanent hazard rather than a one-off mistake, so the assignment is done by the editor: the first letter of the name nothing else claims. | ||
| 68 | + | ||
| 69 | +Tildes written into the name are honoured **when the letter is free**, and quietly overridden when it is not. Refusing the file instead was the alternative, and it is worse: the clash depends on which menus exist, so a tools file that worked would break the day an editor release added a menu. Between a menu on a letter you did not ask for and a menu you cannot open, the first is the smaller loss. | ||
| 70 | + | ||
| 71 | +When every letter of a name is taken, the menu gets no hot key at all. `F10`, the arrow keys and the mouse still reach it, and the alternative — reaching for a letter that is not in the name — would put an underline under nothing. | ||
| 72 | + | ||
| 73 | +## Why the bar is rebuilt from a stat | ||
| 74 | + | ||
| 75 | +`Menu.OnOpen` refills a menu's items just before it drops down, which is how the Golo and Snippets menus follow their files without a restart. It cannot help here: the *set* of menus is part of the bar, not part of any one menu, and adding `menu = "Docker"` to the file should put Docker on the bar. | ||
| 76 | + | ||
| 77 | +Reading and parsing the file on every turn of the event loop would do it, and would also be work done for nothing on every keystroke of a file nobody has edited. So the bar carries the size and modification time of the tools file it was built from, and one `stat` per turn decides whether to rebuild. Editing the file in the window in front of you, saving it, and watching the bar change is the case this is for. | ||
| 78 | + | ||
| 79 | +## Why open files are re-read, and only some of them | ||
| 80 | + | ||
| 81 | +Golo has no formatter, so no starter command rewrites the file in front of you — but `golo new` writes a file into the directory, `gogolo build -keep-go` leaves a `.go` beside the script, and your own tools may do anything at all. Without anything further, the editor would sit on a stale copy of a file another command changed, and the next `F2` would write your copy back over the command's work. | ||
| 82 | + | ||
| 83 | +So when a command finishes, the editor re-reads every open file. The interesting part is which ones it refuses to touch. | ||
| 84 | + | ||
| 85 | +**A file with unsaved changes is left alone**, and the status bar says how many were skipped. Reloading it would throw away work the user has not saved, which no amount of convenience justifies. And the conflict is genuine: the command and the unsaved edit disagree about what the file should say, and the editor is not in a position to decide. Naming it and stopping is the honest outcome. | ||
| 86 | + | ||
| 87 | +Two smaller decisions inside that: | ||
| 88 | + | ||
| 89 | +- **The cursor stays where it was**, clamped into whatever the file now holds. | ||
| 90 | +- **The undo history is discarded.** Undoing back past a reload would restore text the file no longer has, which is worse than not being able to undo at all. | ||
| 91 | + | ||
| 92 | +## Why the reload happens on the event loop | ||
| 93 | + | ||
| 94 | +The command's exit is noticed on the goroutine reading the terminal, which may not touch a buffer or the desktop. So it sets a flag, and the reload runs at the top of the next turn of the event loop. | ||
| 95 | + | ||
| 96 | +This is the fourth thing in the library built that way — the language-server announcement, the terminal redraws, the autosave deadline, and now this. The rule they share is worth stating once more: **the wake-up may be lost, so the state must not be.** `PostEvent` drops what does not fit in its queue, so anything that depends on a message arriving is a bug waiting for a busy moment. A flag the loop checks for itself cannot go missing. | ||
| 97 | + | ||
| 98 | +## Why a command can ask for a value, and why it asks in double braces | ||
| 99 | + | ||
| 100 | +`golo` needs a script. `golo new` needs a module name and a file name. `gogolo build` needs a script and an output path. `wagolo build` needs a target too. None of those can live in the tools file as a fixed string, because the answer is different every time — and a tool that cannot ask is a tool that has to be edited before each use, which is not a tool. | ||
| 101 | + | ||
| 102 | +So a `{{label}}` in a command is a value the editor asks for first, in a box titled after the tool. **Six of the eight Golo commands use it**, which is deliberate: a feature demonstrated in the file everybody gets is a feature people find, and one described only in a comment is not. That is more placeholders than any sibling's starter file carries, and the reason is Golo's: with no manifest there is no `moon run` that knows what to run, so every command that touches a file has to be told which one. | ||
| 103 | + | ||
| 104 | +**Single braces were the obvious spelling and are wrong.** `awk '{print $1}'` and `find . -exec rm {} +` are ordinary things to put in a tools file, and reading the first as a placeholder turns a working command into a box asking for "print $1". Double braces collide with almost nothing. | ||
| 105 | + | ||
| 106 | +**The value is quoted by default**, because the alternative fails silently. A path with a space in it, substituted raw, becomes two arguments and the command reports something about a file that does not exist. Quoting makes that case work and makes the other case — "put these three flags on the end" — impossible, so `...` inside the braces asks for the value verbatim. Two behaviours, both documented, rather than one that is wrong half the time. | ||
| 107 | + | ||
| 108 | +**Nothing is remembered on disk.** The box starts from what was typed last time, for the session. Writing it into the project's own directory was considered and rejected: that directory holds what the project decided, and the script somebody ran while chasing one bug is not that. | ||
| 109 | + | ||
| 110 | +**A file that cannot be parsed is refused when it is read**, not when the tool is chosen. An unclosed `{{` reaching the shell is a command failing with braces in it, which names neither the tool nor the file; refusing at load names both. That is the same rule an unknown `output` value already follows. | ||
| 111 | + | ||
| 112 | +**The dialog is refused when it will not fit.** A tool asking for more values than the terminal has rows would give a box whose OK button is below the bottom of the screen — answerable only by Escape, which cancels. Saying "this asks for twelve values and nine fit" is worse than nothing only if you would rather find out by trying. | ||
| 113 | + | ||
| 114 | +## How it relates to the rest | ||
| 115 | + | ||
| 116 | +- Every key of the file and every rule: [Golo tools reference](../reference/golo-tools.md) | ||
| 117 | +- Using it: [How to run Golo commands from the editor](../how-to/run-golo-commands.md) | ||
| 118 | +- The windows `output = "terminal"` uses, and why they are real terminals: [Terminal windows](terminal-windows.md) | ||
| 119 | +- The other menu built from a file: [Snippets](snippets.md) | ||
added
docs/en/explanation/project-settings.md +70 -0 | new file mode 100644 | ||
| @@ -0,0 +1,70 @@ | ||
| 1 | +# Project settings — explanation | |
| 2 | + | |
| 3 | +## What is this about? | |
| 4 | + | |
| 5 | +A project can keep a `.turbo-golo/settings.toml` beside its code, saying which theme to use and whether to save files automatically. This page is about the decisions inside that sentence: why the file is only ever looked for in one place, why creating it is a menu item rather than something that happens by itself, and why automatic saving works the way it does. | |
| 6 | + | |
| 7 | +## Why the directory is not searched for upwards | |
| 8 | + | |
| 9 | +Turbo Golo has two answers to "where are we", and neither of them walks up. | |
| 10 | + | |
| 11 | +The language server is started in the directory of the file you opened. Golo has no project manifest — no module file, no build file — so there is nothing to walk up *to*, and `golo lsp` does not need a root anyway: it answers about the file it is given and resolves imports from the modules embedded in the binary, never from disk. | |
| 12 | + | |
| 13 | +The settings file is looked for in the working directory, and nowhere else. A walk would have been possible here — up to the nearest `.turbo-golo`, say — and it was deliberately not done. "The project" is not a fact about the code. It is where you decided to start working, and the same directory tree can be several projects depending on what you are doing in it — a monorepo's `services/api` is a project when you are working on the API and part of a larger one when you are not. | |
| 14 | + | |
| 15 | +A walk would also make the setting act at a distance. You open a file, and the editor's colours change because of a file three directories up that you did not know existed. Every explanation of that behaviour has to start with "well, it searches upwards", and the rule you would rather be able to state is the one that is now true: **the project is the directory you started the editor in.** | |
| 16 | + | |
| 17 | +The cost is real and worth naming. Start the editor from `demos/shapes` and the project's theme does not apply. The answer is to start from the project root, which is where you would run `golo --test` and `git` anyway. | |
| 18 | + | |
| 19 | +## Why creating the file is a menu item | |
| 20 | + | |
| 21 | +The alternative was tempting: the first time you pick a theme, write `.turbo-golo/settings.toml` so the choice sticks. Every editor that stores workspace state does something like it. | |
| 22 | + | |
| 23 | +It was rejected because it puts a directory into someone's repository as a side effect of trying a colour. The user is one `git status` away from a change they did not make, in a project that may not be theirs, possibly in a review. A theme picked to look at for ten seconds should not leave anything behind. | |
| 24 | + | |
| 25 | +So the file is created only by **Options ▸ Create project settings**, and its existence means something: this project has settings on purpose. That is also what makes the write-back rule simple to state — **the theme is written to the file when the file exists, and not otherwise** — with no flag anywhere for "do you want to remember this?". | |
| 26 | + | |
| 27 | +## Why the theme is written in place rather than re-encoded | |
| 28 | + | |
| 29 | +Once the file exists, picking a theme rewrites it. Marshalling the `Settings` struct back to TOML would be four lines and would delete every comment in the file. | |
| 30 | + | |
| 31 | +That matters more here than it usually would, because this file is *meant* to be edited by hand. It is the reason TOML colouring exists in the editor at all; the created file is mostly comments explaining the keys; a team will add comments of their own saying why they chose what they chose. Losing all of it the first time someone tries a different theme would be a silent, surprising deletion of somebody's writing. | |
| 32 | + | |
| 33 | +So the rewrite finds the `theme` line inside the `[editor]` table and changes the value between the `=` and any trailing comment. Everything else in the file comes back byte for byte. It is about forty lines rather than four, and it is the difference between a file you can keep things in and a file that eats them. | |
| 34 | + | |
| 35 | +## Why automatic saving waits for a pause | |
| 36 | + | |
| 37 | +Three triggers were considered. | |
| 38 | + | |
| 39 | +**On a fixed interval** is the simplest and is wrong: it writes in the middle of an edit. Half a renamed identifier reaches disk, a file watcher rebuilds, and a test suite fails on code that never existed as anyone's intention. | |
| 40 | + | |
| 41 | +**On leaving the window** never writes while you work, which sounds safe and means the thing on disk can be an hour behind the thing on screen — precisely when it matters, because the reason to want autosave is usually a tool watching the file. | |
| 42 | + | |
| 43 | +**After a pause in typing** is what both other editors and this one settled on. Two seconds is long enough that a pause for thought is not a write, short enough that a rebuild follows a change closely. A run of typing is one write, not one per keystroke. | |
| 44 | + | |
| 45 | +There is one deadline for the whole editor rather than one per window, because "you stopped typing" is one event. A per-window deadline would save the file you have moved away from at a different moment from the one in front of you, which nobody could observe and which is more state to keep right. | |
| 46 | + | |
| 47 | +## Why the deadline is checked, and only nudged by a timer | |
| 48 | + | |
| 49 | +This is the same trap the language-server announcement and the terminal redraws both hit, and it is worth stating once more because it will come up again. | |
| 50 | + | |
| 51 | +The editor blocks in `PollEvent`. To notice a deadline while nothing is happening, something has to wake it, and the only way to wake it from a timer is `PostEvent` — which **drops** events when its queue is full. | |
| 52 | + | |
| 53 | +So the timer is not what decides. The deadline is state, checked at the top of every turn of the event loop, exactly as the language-server announcement checks whether the server is ready. The timer's only job is to make sure a turn happens. A nudge that gets dropped costs a save that is late until the next keystroke or click; a design where the timer did the saving would lose it altogether. | |
| 54 | + | |
| 55 | +## Why a failed automatic save does not open a dialog | |
| 56 | + | |
| 57 | +An autosave nobody asked for should not interrupt with a modal, and a modal that reappears every two seconds because a file is read-only is worse than the problem it reports. It goes on the status bar instead, and the deadline is cleared *before* the write is attempted, so a file that cannot be written is tried once per edit rather than forever. | |
| 58 | + | |
| 59 | +## Why TOML colouring reuses the code classes | |
| 60 | + | |
| 61 | +Adding `syntax.tomlkey` and friends would have meant every theme — including the ones users have written — silently failing to colour TOML until it was updated. | |
| 62 | + | |
| 63 | +The classes already there fit: a table header names a structure, so it reads as a type; a key names a thing, so it reads as an identifier; `true` and `false` are constants because that is what they are. The result is that every theme that ever worked colours TOML correctly, with no change and no new keys. The scanner is hand-written for the same reason the terminal emulator is — TOML is a small, fully specified language, and it is one file against a third dependency. | |
| 64 | + | |
| 65 | +## How it relates to the rest | |
| 66 | + | |
| 67 | +- The exact keys and their defaults: [Project settings reference](../reference/project-settings.md) | |
| 68 | +- Setting one up: [How to give a project its own settings](../how-to/configure-a-project.md) | |
| 69 | +- The other TOML file the editor reads: [Theme file format](../reference/themes.md) | |
| 70 | +- Where `settings` sits among the packages: [Architecture](architecture.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | +# Project settings — explanation | ||
| 2 | + | ||
| 3 | +## What is this about? | ||
| 4 | + | ||
| 5 | +A project can keep a `.turbo-golo/settings.toml` beside its code, saying which theme to use and whether to save files automatically. This page is about the decisions inside that sentence: why the file is only ever looked for in one place, why creating it is a menu item rather than something that happens by itself, and why automatic saving works the way it does. | ||
| 6 | + | ||
| 7 | +## Why the directory is not searched for upwards | ||
| 8 | + | ||
| 9 | +Turbo Golo has two answers to "where are we", and neither of them walks up. | ||
| 10 | + | ||
| 11 | +The language server is started in the directory of the file you opened. Golo has no project manifest — no module file, no build file — so there is nothing to walk up *to*, and `golo lsp` does not need a root anyway: it answers about the file it is given and resolves imports from the modules embedded in the binary, never from disk. | ||
| 12 | + | ||
| 13 | +The settings file is looked for in the working directory, and nowhere else. A walk would have been possible here — up to the nearest `.turbo-golo`, say — and it was deliberately not done. "The project" is not a fact about the code. It is where you decided to start working, and the same directory tree can be several projects depending on what you are doing in it — a monorepo's `services/api` is a project when you are working on the API and part of a larger one when you are not. | ||
| 14 | + | ||
| 15 | +A walk would also make the setting act at a distance. You open a file, and the editor's colours change because of a file three directories up that you did not know existed. Every explanation of that behaviour has to start with "well, it searches upwards", and the rule you would rather be able to state is the one that is now true: **the project is the directory you started the editor in.** | ||
| 16 | + | ||
| 17 | +The cost is real and worth naming. Start the editor from `demos/shapes` and the project's theme does not apply. The answer is to start from the project root, which is where you would run `golo --test` and `git` anyway. | ||
| 18 | + | ||
| 19 | +## Why creating the file is a menu item | ||
| 20 | + | ||
| 21 | +The alternative was tempting: the first time you pick a theme, write `.turbo-golo/settings.toml` so the choice sticks. Every editor that stores workspace state does something like it. | ||
| 22 | + | ||
| 23 | +It was rejected because it puts a directory into someone's repository as a side effect of trying a colour. The user is one `git status` away from a change they did not make, in a project that may not be theirs, possibly in a review. A theme picked to look at for ten seconds should not leave anything behind. | ||
| 24 | + | ||
| 25 | +So the file is created only by **Options ▸ Create project settings**, and its existence means something: this project has settings on purpose. That is also what makes the write-back rule simple to state — **the theme is written to the file when the file exists, and not otherwise** — with no flag anywhere for "do you want to remember this?". | ||
| 26 | + | ||
| 27 | +## Why the theme is written in place rather than re-encoded | ||
| 28 | + | ||
| 29 | +Once the file exists, picking a theme rewrites it. Marshalling the `Settings` struct back to TOML would be four lines and would delete every comment in the file. | ||
| 30 | + | ||
| 31 | +That matters more here than it usually would, because this file is *meant* to be edited by hand. It is the reason TOML colouring exists in the editor at all; the created file is mostly comments explaining the keys; a team will add comments of their own saying why they chose what they chose. Losing all of it the first time someone tries a different theme would be a silent, surprising deletion of somebody's writing. | ||
| 32 | + | ||
| 33 | +So the rewrite finds the `theme` line inside the `[editor]` table and changes the value between the `=` and any trailing comment. Everything else in the file comes back byte for byte. It is about forty lines rather than four, and it is the difference between a file you can keep things in and a file that eats them. | ||
| 34 | + | ||
| 35 | +## Why automatic saving waits for a pause | ||
| 36 | + | ||
| 37 | +Three triggers were considered. | ||
| 38 | + | ||
| 39 | +**On a fixed interval** is the simplest and is wrong: it writes in the middle of an edit. Half a renamed identifier reaches disk, a file watcher rebuilds, and a test suite fails on code that never existed as anyone's intention. | ||
| 40 | + | ||
| 41 | +**On leaving the window** never writes while you work, which sounds safe and means the thing on disk can be an hour behind the thing on screen — precisely when it matters, because the reason to want autosave is usually a tool watching the file. | ||
| 42 | + | ||
| 43 | +**After a pause in typing** is what both other editors and this one settled on. Two seconds is long enough that a pause for thought is not a write, short enough that a rebuild follows a change closely. A run of typing is one write, not one per keystroke. | ||
| 44 | + | ||
| 45 | +There is one deadline for the whole editor rather than one per window, because "you stopped typing" is one event. A per-window deadline would save the file you have moved away from at a different moment from the one in front of you, which nobody could observe and which is more state to keep right. | ||
| 46 | + | ||
| 47 | +## Why the deadline is checked, and only nudged by a timer | ||
| 48 | + | ||
| 49 | +This is the same trap the language-server announcement and the terminal redraws both hit, and it is worth stating once more because it will come up again. | ||
| 50 | + | ||
| 51 | +The editor blocks in `PollEvent`. To notice a deadline while nothing is happening, something has to wake it, and the only way to wake it from a timer is `PostEvent` — which **drops** events when its queue is full. | ||
| 52 | + | ||
| 53 | +So the timer is not what decides. The deadline is state, checked at the top of every turn of the event loop, exactly as the language-server announcement checks whether the server is ready. The timer's only job is to make sure a turn happens. A nudge that gets dropped costs a save that is late until the next keystroke or click; a design where the timer did the saving would lose it altogether. | ||
| 54 | + | ||
| 55 | +## Why a failed automatic save does not open a dialog | ||
| 56 | + | ||
| 57 | +An autosave nobody asked for should not interrupt with a modal, and a modal that reappears every two seconds because a file is read-only is worse than the problem it reports. It goes on the status bar instead, and the deadline is cleared *before* the write is attempted, so a file that cannot be written is tried once per edit rather than forever. | ||
| 58 | + | ||
| 59 | +## Why TOML colouring reuses the code classes | ||
| 60 | + | ||
| 61 | +Adding `syntax.tomlkey` and friends would have meant every theme — including the ones users have written — silently failing to colour TOML until it was updated. | ||
| 62 | + | ||
| 63 | +The classes already there fit: a table header names a structure, so it reads as a type; a key names a thing, so it reads as an identifier; `true` and `false` are constants because that is what they are. The result is that every theme that ever worked colours TOML correctly, with no change and no new keys. The scanner is hand-written for the same reason the terminal emulator is — TOML is a small, fully specified language, and it is one file against a third dependency. | ||
| 64 | + | ||
| 65 | +## How it relates to the rest | ||
| 66 | + | ||
| 67 | +- The exact keys and their defaults: [Project settings reference](../reference/project-settings.md) | ||
| 68 | +- Setting one up: [How to give a project its own settings](../how-to/configure-a-project.md) | ||
| 69 | +- The other TOML file the editor reads: [Theme file format](../reference/themes.md) | ||
| 70 | +- Where `settings` sits among the packages: [Architecture](architecture.md) | ||
added
docs/en/explanation/project-tree.md +58 -0 | new file mode 100644 | ||
| @@ -0,0 +1,58 @@ | ||
| 1 | +# Project tree — explanation | |
| 2 | + | |
| 3 | +## What is this about? | |
| 4 | + | |
| 5 | +`F9` opens a window listing the project's files, and pressing `Enter` on one opens it. This page is about the three decisions inside that: where the tree is rooted, why it is a window rather than a panel down the side, and why it does not notice files appearing on its own. | |
| 6 | + | |
| 7 | +## Why the root is the working directory | |
| 8 | + | |
| 9 | +The editor already contains two different answers to "what is the project". | |
| 10 | + | |
| 11 | +The language server is started in the directory of the file you opened. Golo has no project manifest, so there is nothing to walk up to, and `golo lsp` needs no root: it answers about the file it is given. The project settings file does not walk either: `.turbo-golo/settings.toml` is looked for in the working directory and nowhere else. | |
| 12 | + | |
| 13 | +The tree follows the settings file, and it is worth saying why another rule was tempting. An explorer usually roots itself at something it finds by walking up — a manifest, or the nearest `.git` — so that opening a file from anywhere in a project shows the whole project. Golo offers no manifest to find, and rooting at `.git` would make the tree's root depend on a directory three levels away that you may not have thought about; it stops being predictable the moment a repository holds more than one program — a monorepo would show you the whole repository whichever script you happened to open. | |
| 14 | + | |
| 15 | +The rule kept is the one that can be said in a sentence and is true everywhere in the editor: **the project is the directory you started the editor in.** It costs something, and the cost is named in the [how-to](../how-to/browse-a-project.md): start from a subdirectory and you get a tree of that subdirectory. The answer is to start from the project root, which is where you would run `golo --test` and `git` anyway. | |
| 16 | + | |
| 17 | +## Why `.git` is hidden and nothing else is | |
| 18 | + | |
| 19 | +The Open dialog hides every entry beginning with a dot. Copying that here was the obvious thing and would have been wrong. | |
| 20 | + | |
| 21 | +`.turbo-golo/settings.toml` is a file this editor asks people to edit — it is why the editor colours TOML at all. `.gitignore` and `.qlty/qlty.toml` are files of the project too. A tree that hid them would make the editor's own configuration unreachable from the editor's own file browser, which is an odd place to end up. | |
| 22 | + | |
| 23 | +`.git` is different in kind rather than in spelling: nothing inside it is meant to be opened by hand, and it holds enough objects to bury everything else in the listing. One name, hidden for a reason that can be stated. Respecting `.gitignore` as well was considered and turned down for now: it would hide `bin/` and `release/`, which is genuinely nicer, and it costs a gitignore pattern engine — negation, `**`, anchoring — that is a feature in its own right rather than a detail of a tree. | |
| 24 | + | |
| 25 | +## Why it is a window, not a panel | |
| 26 | + | |
| 27 | +Every other editor puts its file tree in a fixed strip down the left. That was the alternative, and it was turned down because of what it would have cost the rest of the editor. | |
| 28 | + | |
| 29 | +A docked panel means the desktop is no longer a single rectangle that windows live in. `Desktop` would need a notion of reserved edges; `Window.fitInto` and the grow modes would have to respect them; maximising would mean "the whole desktop except the panel"; tiling and cascading would need to know about it. That is a change to the foundation of the whole interface, for one widget. | |
| 30 | + | |
| 31 | +As an ordinary window, the tree gets everything for free and behaves like everything else: `F6` reaches it, `Alt-2` raises it, `[x]` closes it, `[■]` fills the desktop with it, **Window ▸ Tile** puts it beside your file. Nothing in `ui` had to change. If a docked panel is wanted later, it is a `ui` feature to be designed on its own terms rather than something smuggled in with a file browser. | |
| 32 | + | |
| 33 | +## Why there is only one | |
| 34 | + | |
| 35 | +Two trees on the same project would be two views of one thing with nothing to tell them apart, and the project cannot change while the editor runs — the root is fixed at start-up. So `F9` on an open tree raises it rather than making another, the same way opening a file that is already open raises its window. | |
| 36 | + | |
| 37 | +## Why it does not watch the disk | |
| 38 | + | |
| 39 | +A tree that noticed `gogolo build` producing an executable would be better. Doing it properly means watching the filesystem, and in Go that means `fsnotify` — a third dependency, against a project that has kept to two since it started and treats adding one as a decision to be argued for. | |
| 40 | + | |
| 41 | +It is not a small dependency in behaviour either: recursive watches, watch descriptors running out on large trees, and different semantics on every platform, in a feature whose failure mode is a stale line in a list. | |
| 42 | + | |
| 43 | +So the tree re-reads on demand, and the editor picks the moments it can be sure about. Saving a file is one: the editor did it, so it knows. `F5` and `Ctrl-R` are the other, because a build in a terminal window is something only the user knows has finished. Refreshing keeps the shape of the tree and re-reads only the directories that were actually opened, so it costs what is on screen rather than a walk of the project. | |
| 44 | + | |
| 45 | +## Why the tree has theme keys of its own | |
| 46 | + | |
| 47 | +The obvious economy was to draw it with the `list.*` keys — a tree is a list, after all, and it would have meant no new keys for user themes to miss. | |
| 48 | + | |
| 49 | +It does not work, and the reason is worth recording. `list.selected` is coloured to stand out against a **dialog**. In `turbo-classic` it is white on navy, and `window.body` is silver on **navy** — a tree in a window would have highlighted its selected row in exactly the background colour it sits on. The selection would have been invisible in the theme the editor ships as its default. | |
| 50 | + | |
| 51 | +So `tree.text`, `tree.directory`, `tree.selected` and `tree.unfocused` exist, and a test holds every shipped theme to a minimum contrast between the first and the third, in the same way the cursor colours are checked. A user theme that sets none of them falls back along the dots to `default`: a readable tree without the file-and-directory distinction, rather than nothing at all. | |
| 52 | + | |
| 53 | +## How it relates to the rest | |
| 54 | + | |
| 55 | +- Every key and every rule, exactly: [Project tree reference](../reference/project-tree.md) | |
| 56 | +- Using it: [How to browse a project and open files from a tree](../how-to/browse-a-project.md) | |
| 57 | +- The other place "the project" is defined the same way: [Project settings](project-settings.md) | |
| 58 | +- Where `filetree` sits among the packages: [Architecture](architecture.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,58 @@ | |||
| 1 | +# Project tree — explanation | ||
| 2 | + | ||
| 3 | +## What is this about? | ||
| 4 | + | ||
| 5 | +`F9` opens a window listing the project's files, and pressing `Enter` on one opens it. This page is about the three decisions inside that: where the tree is rooted, why it is a window rather than a panel down the side, and why it does not notice files appearing on its own. | ||
| 6 | + | ||
| 7 | +## Why the root is the working directory | ||
| 8 | + | ||
| 9 | +The editor already contains two different answers to "what is the project". | ||
| 10 | + | ||
| 11 | +The language server is started in the directory of the file you opened. Golo has no project manifest, so there is nothing to walk up to, and `golo lsp` needs no root: it answers about the file it is given. The project settings file does not walk either: `.turbo-golo/settings.toml` is looked for in the working directory and nowhere else. | ||
| 12 | + | ||
| 13 | +The tree follows the settings file, and it is worth saying why another rule was tempting. An explorer usually roots itself at something it finds by walking up — a manifest, or the nearest `.git` — so that opening a file from anywhere in a project shows the whole project. Golo offers no manifest to find, and rooting at `.git` would make the tree's root depend on a directory three levels away that you may not have thought about; it stops being predictable the moment a repository holds more than one program — a monorepo would show you the whole repository whichever script you happened to open. | ||
| 14 | + | ||
| 15 | +The rule kept is the one that can be said in a sentence and is true everywhere in the editor: **the project is the directory you started the editor in.** It costs something, and the cost is named in the [how-to](../how-to/browse-a-project.md): start from a subdirectory and you get a tree of that subdirectory. The answer is to start from the project root, which is where you would run `golo --test` and `git` anyway. | ||
| 16 | + | ||
| 17 | +## Why `.git` is hidden and nothing else is | ||
| 18 | + | ||
| 19 | +The Open dialog hides every entry beginning with a dot. Copying that here was the obvious thing and would have been wrong. | ||
| 20 | + | ||
| 21 | +`.turbo-golo/settings.toml` is a file this editor asks people to edit — it is why the editor colours TOML at all. `.gitignore` and `.qlty/qlty.toml` are files of the project too. A tree that hid them would make the editor's own configuration unreachable from the editor's own file browser, which is an odd place to end up. | ||
| 22 | + | ||
| 23 | +`.git` is different in kind rather than in spelling: nothing inside it is meant to be opened by hand, and it holds enough objects to bury everything else in the listing. One name, hidden for a reason that can be stated. Respecting `.gitignore` as well was considered and turned down for now: it would hide `bin/` and `release/`, which is genuinely nicer, and it costs a gitignore pattern engine — negation, `**`, anchoring — that is a feature in its own right rather than a detail of a tree. | ||
| 24 | + | ||
| 25 | +## Why it is a window, not a panel | ||
| 26 | + | ||
| 27 | +Every other editor puts its file tree in a fixed strip down the left. That was the alternative, and it was turned down because of what it would have cost the rest of the editor. | ||
| 28 | + | ||
| 29 | +A docked panel means the desktop is no longer a single rectangle that windows live in. `Desktop` would need a notion of reserved edges; `Window.fitInto` and the grow modes would have to respect them; maximising would mean "the whole desktop except the panel"; tiling and cascading would need to know about it. That is a change to the foundation of the whole interface, for one widget. | ||
| 30 | + | ||
| 31 | +As an ordinary window, the tree gets everything for free and behaves like everything else: `F6` reaches it, `Alt-2` raises it, `[x]` closes it, `[■]` fills the desktop with it, **Window ▸ Tile** puts it beside your file. Nothing in `ui` had to change. If a docked panel is wanted later, it is a `ui` feature to be designed on its own terms rather than something smuggled in with a file browser. | ||
| 32 | + | ||
| 33 | +## Why there is only one | ||
| 34 | + | ||
| 35 | +Two trees on the same project would be two views of one thing with nothing to tell them apart, and the project cannot change while the editor runs — the root is fixed at start-up. So `F9` on an open tree raises it rather than making another, the same way opening a file that is already open raises its window. | ||
| 36 | + | ||
| 37 | +## Why it does not watch the disk | ||
| 38 | + | ||
| 39 | +A tree that noticed `gogolo build` producing an executable would be better. Doing it properly means watching the filesystem, and in Go that means `fsnotify` — a third dependency, against a project that has kept to two since it started and treats adding one as a decision to be argued for. | ||
| 40 | + | ||
| 41 | +It is not a small dependency in behaviour either: recursive watches, watch descriptors running out on large trees, and different semantics on every platform, in a feature whose failure mode is a stale line in a list. | ||
| 42 | + | ||
| 43 | +So the tree re-reads on demand, and the editor picks the moments it can be sure about. Saving a file is one: the editor did it, so it knows. `F5` and `Ctrl-R` are the other, because a build in a terminal window is something only the user knows has finished. Refreshing keeps the shape of the tree and re-reads only the directories that were actually opened, so it costs what is on screen rather than a walk of the project. | ||
| 44 | + | ||
| 45 | +## Why the tree has theme keys of its own | ||
| 46 | + | ||
| 47 | +The obvious economy was to draw it with the `list.*` keys — a tree is a list, after all, and it would have meant no new keys for user themes to miss. | ||
| 48 | + | ||
| 49 | +It does not work, and the reason is worth recording. `list.selected` is coloured to stand out against a **dialog**. In `turbo-classic` it is white on navy, and `window.body` is silver on **navy** — a tree in a window would have highlighted its selected row in exactly the background colour it sits on. The selection would have been invisible in the theme the editor ships as its default. | ||
| 50 | + | ||
| 51 | +So `tree.text`, `tree.directory`, `tree.selected` and `tree.unfocused` exist, and a test holds every shipped theme to a minimum contrast between the first and the third, in the same way the cursor colours are checked. A user theme that sets none of them falls back along the dots to `default`: a readable tree without the file-and-directory distinction, rather than nothing at all. | ||
| 52 | + | ||
| 53 | +## How it relates to the rest | ||
| 54 | + | ||
| 55 | +- Every key and every rule, exactly: [Project tree reference](../reference/project-tree.md) | ||
| 56 | +- Using it: [How to browse a project and open files from a tree](../how-to/browse-a-project.md) | ||
| 57 | +- The other place "the project" is defined the same way: [Project settings](project-settings.md) | ||
| 58 | +- Where `filetree` sits among the packages: [Architecture](architecture.md) | ||
added
docs/en/explanation/snippets.md +70 -0 | new file mode 100644 | ||
| @@ -0,0 +1,70 @@ | ||
| 1 | +# Snippets — explanation | |
| 2 | + | |
| 3 | +## What is this about? | |
| 4 | + | |
| 5 | +A **Snippets** menu whose contents come from a TOML file, and a chosen snippet dropped into the file you are editing. This page is about the decisions that shape it: why the menu is rebuilt every time it opens, why the editor grew real submenus for it, why insertion re-indents, and why the Golo bodies in the starter file are written the way they are. | |
| 6 | + | |
| 7 | +## Why the menu is built at the moment it opens | |
| 8 | + | |
| 9 | +Every other menu in the editor is decided once, in `New()`. This one cannot be, and there are two independent reasons. | |
| 10 | + | |
| 11 | +The first is the file. Snippets live in TOML, and the whole point of that is that you edit it — often in this editor, in the window the **Create snippets file** item just opened for you. A menu built at start-up would show the state of the file when the editor launched, and you would have to restart to see a snippet you had just written. That is the kind of friction that stops people using a feature at all. | |
| 12 | + | |
| 13 | +The second is the front window. The menu is filtered by what you are editing, so it changes when you press `F6`. There is no start-up moment at which the answer exists. | |
| 14 | + | |
| 15 | +So `ui.Menu` grew an `OnOpen` field: a function the bar calls immediately before dropping a menu down, letting its owner refill `Items` first. It is the same upward-communication mechanism as everything else in this codebase — a function field, not an interface — and it runs at exactly the moment the contents are about to be seen and no more often. | |
| 16 | + | |
| 17 | +## Why the editor grew submenus | |
| 18 | + | |
| 19 | +`ui.MenuItem` had no nesting, and adding it was the largest single piece of this work: a second panel to place and draw, arrow keys that mean "further in" and "back out", the pointer opening a branch on hover and closing it on leaving, and a cascade that puts both panels away at once. | |
| 20 | + | |
| 21 | +The alternative was one flat panel with the groups as greyed-out captions between separators. It works, needs nothing new, and falls over on the case the feature is for: a project with thirty snippets gives a menu taller than the terminal. Grouping that only labels rather than folds does not solve the problem it appears to solve. | |
| 22 | + | |
| 23 | +It is deliberately **one level deep**. The format is groups containing snippets — exactly one level — and a general depth would mean replacing the bar's two indices with a path, in the widget every dialog and every menu test already depends on. That is speculative work on the most load-bearing part of the interface. | |
| 24 | + | |
| 25 | +Two details of the submenu are worth naming because they were chosen rather than fallen into: | |
| 26 | + | |
| 27 | +- **Right and left are asymmetric with Escape.** Right opens a branch, or moves to the next menu when the item has none, so it always means "further in" wherever you are. Left steps *out* of a submenu to its parent, while Escape puts the whole menu away — because cancel should mean cancel from anywhere. | |
| 28 | +- **The panel flips left, and is also capped to the screen.** A submenu that would run off the right edge is drawn on the other side of its parent instead. Flipping alone is not enough: a panel wider than the terminal cannot be made to fit by moving it, so the width is capped too and long labels are clipped by the painter. A frame with no right-hand edge looks broken in a way a truncated label does not. | |
| 29 | + | |
| 30 | +## Why insertion re-indents | |
| 31 | + | |
| 32 | +A snippet is text, and the obvious implementation is to insert it. That is right for a one-liner and wrong for everything else, which is most of what people keep in snippets. | |
| 33 | + | |
| 34 | +Dropped in verbatim, a multi-line body restarts at column zero. Inserted inside a function, inside a `foreach`, inside a `try` block — which is where you insert a `match` — the result is text that no reader is happy with, and Golo has no formatter to put it right afterwards: what you insert is what stays in the file. The first thing you do is re-indent it by hand, and a feature whose output needs fixing every time is not saving anyone anything. | |
| 35 | + | |
| 36 | +So the lines after the first get the leading whitespace of the line the cursor was on. That copies whatever the file already uses — tabs or spaces, however many — rather than imposing a choice, which matters in a project with a mixed history. | |
| 37 | + | |
| 38 | +Two smaller decisions inside that: | |
| 39 | + | |
| 40 | +- **A blank line in the body stays blank.** Padding it to the indent would put trailing whitespace in — noise in the diff of the very next save, and with no formatter in the Golo toolchain, nothing would ever strip it back out. | |
| 41 | +- **It is one undo step.** A snippet is one action to the person who chose it, so `Ctrl-Z` should take all of it back. This falls out of doing the whole insertion in a single `ReplaceRange`, which is the rule the buffer already enforces for every other edit. | |
| 42 | + | |
| 43 | +Placeholders and tab stops — `${1:name}` and moving between them — were considered and left out. They are a second feature with their own state to keep across edits, and the thing being asked for was reusable text. | |
| 44 | + | |
| 45 | +## Why the Golo bodies are indented two spaces, in literal strings | |
| 46 | + | |
| 47 | +The starter file the **Snippets** menu writes holds twelve Golo snippets — `module`, `main`, `function`, `closure`, `struct`, `union`, `augment`, `match`, `foreach`, `for`, `try`, `comprehension` — and two decisions about their text are worth stating. | |
| 48 | + | |
| 49 | +**Two spaces**, because that is what every example in the GoloScript documentation and its own templates uses, and there is no formatter to disagree with. Turbo Go's snippets use tabs because `gofmt` would rewrite anything else; Golo has no `golo fmt`, so the convention is the only authority there is, and the snippets follow it rather than inventing a house style. | |
| 50 | + | |
| 51 | +**Literal strings** — `'''…'''` rather than `"""…"""` — because a Golo string carries `\n` and `\"` the way a Go string does, and TOML resolves exactly those escapes in a basic string before the editor ever sees them. A `try` body containing `println("caught: \"" + e + "\"")` written in a basic string would arrive with real quotation marks in it and no longer parse. In a literal string a backslash is just a backslash, which is what a Golo snippet needs. | |
| 52 | + | |
| 53 | +## Why two files, and why the project wins | |
| 54 | + | |
| 55 | +Your own snippets belong to you and should follow you between projects; a project's belong to the project and should arrive with a checkout. Neither is the whole answer, so both are read. | |
| 56 | + | |
| 57 | +Where a name clashes in the same group, the project's replaces yours. It is the more specific of the two statements, and it is the one a team agreed on — the same reason a `-theme` flag beats a project's setting while a project's setting beats the built-in default. | |
| 58 | + | |
| 59 | +## Why an unreadable file is loud | |
| 60 | + | |
| 61 | +A typo in TOML could drop every snippet silently and leave a menu with nothing but **Create snippets file** — which looks exactly like a project that has no snippets, and sends you to create a file you already have. | |
| 62 | + | |
| 63 | +So the menu shows a greyed-out `Cannot read snippets` where the groups would be. It cannot be chosen, it is where you were looking, and the create item is still below it so there is a way forward either way. | |
| 64 | + | |
| 65 | +## How it relates to the rest | |
| 66 | + | |
| 67 | +- Every key and every rule: [Snippets reference](../reference/snippets.md) | |
| 68 | +- Setting them up: [How to insert snippets from a menu](../how-to/use-snippets.md) | |
| 69 | +- The other file in the same directory: [Project settings](project-settings.md) | |
| 70 | +- The language names `languages` uses: [Languages coloured](../reference/languages.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | +# Snippets — explanation | ||
| 2 | + | ||
| 3 | +## What is this about? | ||
| 4 | + | ||
| 5 | +A **Snippets** menu whose contents come from a TOML file, and a chosen snippet dropped into the file you are editing. This page is about the decisions that shape it: why the menu is rebuilt every time it opens, why the editor grew real submenus for it, why insertion re-indents, and why the Golo bodies in the starter file are written the way they are. | ||
| 6 | + | ||
| 7 | +## Why the menu is built at the moment it opens | ||
| 8 | + | ||
| 9 | +Every other menu in the editor is decided once, in `New()`. This one cannot be, and there are two independent reasons. | ||
| 10 | + | ||
| 11 | +The first is the file. Snippets live in TOML, and the whole point of that is that you edit it — often in this editor, in the window the **Create snippets file** item just opened for you. A menu built at start-up would show the state of the file when the editor launched, and you would have to restart to see a snippet you had just written. That is the kind of friction that stops people using a feature at all. | ||
| 12 | + | ||
| 13 | +The second is the front window. The menu is filtered by what you are editing, so it changes when you press `F6`. There is no start-up moment at which the answer exists. | ||
| 14 | + | ||
| 15 | +So `ui.Menu` grew an `OnOpen` field: a function the bar calls immediately before dropping a menu down, letting its owner refill `Items` first. It is the same upward-communication mechanism as everything else in this codebase — a function field, not an interface — and it runs at exactly the moment the contents are about to be seen and no more often. | ||
| 16 | + | ||
| 17 | +## Why the editor grew submenus | ||
| 18 | + | ||
| 19 | +`ui.MenuItem` had no nesting, and adding it was the largest single piece of this work: a second panel to place and draw, arrow keys that mean "further in" and "back out", the pointer opening a branch on hover and closing it on leaving, and a cascade that puts both panels away at once. | ||
| 20 | + | ||
| 21 | +The alternative was one flat panel with the groups as greyed-out captions between separators. It works, needs nothing new, and falls over on the case the feature is for: a project with thirty snippets gives a menu taller than the terminal. Grouping that only labels rather than folds does not solve the problem it appears to solve. | ||
| 22 | + | ||
| 23 | +It is deliberately **one level deep**. The format is groups containing snippets — exactly one level — and a general depth would mean replacing the bar's two indices with a path, in the widget every dialog and every menu test already depends on. That is speculative work on the most load-bearing part of the interface. | ||
| 24 | + | ||
| 25 | +Two details of the submenu are worth naming because they were chosen rather than fallen into: | ||
| 26 | + | ||
| 27 | +- **Right and left are asymmetric with Escape.** Right opens a branch, or moves to the next menu when the item has none, so it always means "further in" wherever you are. Left steps *out* of a submenu to its parent, while Escape puts the whole menu away — because cancel should mean cancel from anywhere. | ||
| 28 | +- **The panel flips left, and is also capped to the screen.** A submenu that would run off the right edge is drawn on the other side of its parent instead. Flipping alone is not enough: a panel wider than the terminal cannot be made to fit by moving it, so the width is capped too and long labels are clipped by the painter. A frame with no right-hand edge looks broken in a way a truncated label does not. | ||
| 29 | + | ||
| 30 | +## Why insertion re-indents | ||
| 31 | + | ||
| 32 | +A snippet is text, and the obvious implementation is to insert it. That is right for a one-liner and wrong for everything else, which is most of what people keep in snippets. | ||
| 33 | + | ||
| 34 | +Dropped in verbatim, a multi-line body restarts at column zero. Inserted inside a function, inside a `foreach`, inside a `try` block — which is where you insert a `match` — the result is text that no reader is happy with, and Golo has no formatter to put it right afterwards: what you insert is what stays in the file. The first thing you do is re-indent it by hand, and a feature whose output needs fixing every time is not saving anyone anything. | ||
| 35 | + | ||
| 36 | +So the lines after the first get the leading whitespace of the line the cursor was on. That copies whatever the file already uses — tabs or spaces, however many — rather than imposing a choice, which matters in a project with a mixed history. | ||
| 37 | + | ||
| 38 | +Two smaller decisions inside that: | ||
| 39 | + | ||
| 40 | +- **A blank line in the body stays blank.** Padding it to the indent would put trailing whitespace in — noise in the diff of the very next save, and with no formatter in the Golo toolchain, nothing would ever strip it back out. | ||
| 41 | +- **It is one undo step.** A snippet is one action to the person who chose it, so `Ctrl-Z` should take all of it back. This falls out of doing the whole insertion in a single `ReplaceRange`, which is the rule the buffer already enforces for every other edit. | ||
| 42 | + | ||
| 43 | +Placeholders and tab stops — `${1:name}` and moving between them — were considered and left out. They are a second feature with their own state to keep across edits, and the thing being asked for was reusable text. | ||
| 44 | + | ||
| 45 | +## Why the Golo bodies are indented two spaces, in literal strings | ||
| 46 | + | ||
| 47 | +The starter file the **Snippets** menu writes holds twelve Golo snippets — `module`, `main`, `function`, `closure`, `struct`, `union`, `augment`, `match`, `foreach`, `for`, `try`, `comprehension` — and two decisions about their text are worth stating. | ||
| 48 | + | ||
| 49 | +**Two spaces**, because that is what every example in the GoloScript documentation and its own templates uses, and there is no formatter to disagree with. Turbo Go's snippets use tabs because `gofmt` would rewrite anything else; Golo has no `golo fmt`, so the convention is the only authority there is, and the snippets follow it rather than inventing a house style. | ||
| 50 | + | ||
| 51 | +**Literal strings** — `'''…'''` rather than `"""…"""` — because a Golo string carries `\n` and `\"` the way a Go string does, and TOML resolves exactly those escapes in a basic string before the editor ever sees them. A `try` body containing `println("caught: \"" + e + "\"")` written in a basic string would arrive with real quotation marks in it and no longer parse. In a literal string a backslash is just a backslash, which is what a Golo snippet needs. | ||
| 52 | + | ||
| 53 | +## Why two files, and why the project wins | ||
| 54 | + | ||
| 55 | +Your own snippets belong to you and should follow you between projects; a project's belong to the project and should arrive with a checkout. Neither is the whole answer, so both are read. | ||
| 56 | + | ||
| 57 | +Where a name clashes in the same group, the project's replaces yours. It is the more specific of the two statements, and it is the one a team agreed on — the same reason a `-theme` flag beats a project's setting while a project's setting beats the built-in default. | ||
| 58 | + | ||
| 59 | +## Why an unreadable file is loud | ||
| 60 | + | ||
| 61 | +A typo in TOML could drop every snippet silently and leave a menu with nothing but **Create snippets file** — which looks exactly like a project that has no snippets, and sends you to create a file you already have. | ||
| 62 | + | ||
| 63 | +So the menu shows a greyed-out `Cannot read snippets` where the groups would be. It cannot be chosen, it is where you were looking, and the create item is still below it so there is a way forward either way. | ||
| 64 | + | ||
| 65 | +## How it relates to the rest | ||
| 66 | + | ||
| 67 | +- Every key and every rule: [Snippets reference](../reference/snippets.md) | ||
| 68 | +- Setting them up: [How to insert snippets from a menu](../how-to/use-snippets.md) | ||
| 69 | +- The other file in the same directory: [Project settings](project-settings.md) | ||
| 70 | +- The language names `languages` uses: [Languages coloured](../reference/languages.md) | ||
added
docs/en/explanation/terminal-windows.md +72 -0 | new file mode 100644 | ||
| @@ -0,0 +1,72 @@ | ||
| 1 | +# Terminal windows — explanation | |
| 2 | + | |
| 3 | +## What is this about? | |
| 4 | + | |
| 5 | +`F8` opens a window with a shell in it. That sentence hides most of the work: to put a shell in a window, an editor has to become a terminal emulator, and this page is about what that involved and which of the cheaper alternatives were turned down on the way. | |
| 6 | + | |
| 7 | +## Why a real pseudo-terminal | |
| 8 | + | |
| 9 | +The obvious cheap version is to run a command with `exec.Command`, capture its output, and show it in a read-only pane. Many editors ship exactly that, and it fails on the things people actually want a terminal for. | |
| 10 | + | |
| 11 | +A program behaves differently when its output is a pipe rather than a terminal. Coloured output goes plain. `git log` does not page. `ls` prints one name per line. Nothing interactive works at all: no `vim`, no `ssh`, no `git rebase -i`, no answering a prompt, and no `Ctrl-C`, because with no controlling terminal there is no signal to send. For Golo the case is sharper still: `golo` with no file is a read-eval-print loop and `golo --debug` a step debugger, and both read the keyboard — neither is usable on a captured pipe, which is why the starter tools file sends them to a terminal window rather than a popup. | |
| 12 | + | |
| 13 | +So the shell gets a real pseudo-terminal: `/dev/ptmx` on both supported platforms, the child in a session of its own with the slave as its controlling terminal, and `TIOCSWINSZ` whenever the window is resized. That buys job control, `isatty`, `SIGWINCH` and colour, all for free, because they are the same mechanisms every other terminal uses. | |
| 14 | + | |
| 15 | +The cost is that the editor must then read back what a terminal is expected to understand — which is the emulator. | |
| 16 | + | |
| 17 | +## Why write the emulator rather than borrow one | |
| 18 | + | |
| 19 | +Go has terminal emulator libraries. Taking one would have meant a third dependency, against a project that has exactly two and a stated reluctance to add a third. | |
| 20 | + | |
| 21 | +The thing being weighed is not "emulator" against "no emulator" but against *how much* emulator. What a shell, the `golo` REPL, `git`, `less`, `htop` and `vim` need is a well-bounded list: cursor movement, the erase and insert-delete family, a scroll region, SGR in all three colour depths, the alternate screen, auto-wrap, cursor visibility and application cursor keys. That is about six hundred lines, it is written down in ECMA-48, and it is testable by writing bytes in and reading a grid out — no shell, no timing, no screen. | |
| 22 | + | |
| 23 | +Compare that with what a general-purpose library brings: character sets, mouse reporting protocols, sixel, bracketed paste, DEC status reports. All real, none of it needed here, and all of it surface to keep working. | |
| 24 | + | |
| 25 | +So the emulator is hand-written and deliberately partial, and the [reference](../reference/terminal.md) says exactly where it stops. A program that asks for something absent gets silence rather than corruption, which is the failure mode worth having: `htop` renders, `sixel` output simply does not appear. | |
| 26 | + | |
| 27 | +## Who gets the key press | |
| 28 | + | |
| 29 | +This is the decision with the most consequence for how the editor feels, and the first version got it wrong. | |
| 30 | + | |
| 31 | +The editor's global shortcuts are checked before the window in front sees anything. That is right for an editor and wrong the moment the window in front is a shell, because the two disagree about the same keys. `Ctrl-W` closes a window in Turbo C and deletes a word in every shell. `Ctrl-F` is Find here and forward-a-character in readline. `Ctrl-C` is copy, and also the only way to stop a runaway command. | |
| 32 | + | |
| 33 | +The rule chosen inverts the usual order, but only for the keys that are genuinely contested: | |
| 34 | + | |
| 35 | +**A focused terminal gets everything except the function keys, `Alt-X`, and `Alt-0`…`Alt-9`.** | |
| 36 | + | |
| 37 | +Those exceptions are not a compromise between the two claims — they are the way *out*. A full-screen program like `vim` covers the window and takes the mouse; without a reserved key there would be no way to reach the menu bar, switch windows or leave the editor short of quitting the program inside. Function keys are the natural reservation because a terminal user reaches for them least, and `Alt-X` because leaving an editor should never be in doubt. | |
| 38 | + | |
| 39 | +What this costs is real and worth naming: `Alt-B` and `Alt-F` reach the shell, so readline's word movement works, but a program inside a terminal window can never see `F1`…`F12`. `htop`'s function-key menu is unreachable. That is the trade, and it was made in favour of always being able to get out. | |
| 40 | + | |
| 41 | +## Why closing a terminal asks nothing | |
| 42 | + | |
| 43 | +Closing a modified file asks whether to save it. Closing a terminal does not ask anything at all, and that asymmetry is deliberate. | |
| 44 | + | |
| 45 | +A window with unsaved work holds something that would be *lost*. A terminal holds a running process, and closing the window is the ordinary way to say you are done with it — the same as closing a terminal emulator's tab. Asking "are you sure?" every time would train the answer out of anyone, which is the general problem with confirmations that fire on the common case. | |
| 46 | + | |
| 47 | +Leaving the editor closes every terminal for the same reason in reverse: a window is the only handle on those shells, so letting them outlive the editor would strand the processes with nothing able to reach them. | |
| 48 | + | |
| 49 | +## Why the redraws are on a clock | |
| 50 | + | |
| 51 | +The shell writes on a goroutine of its own; the editor draws on the main one. Waking the event loop per chunk of output looked obvious and was wrong twice over. | |
| 52 | + | |
| 53 | +A build writes far faster than a screen can usefully be repainted, so most of those redraws are wasted. Worse, the mechanism for waking the loop from another goroutine is tcell's `PostEvent`, which **drops** events when its queue is full — so the burst that most needs a redraw is the one whose final wake-up gets discarded, and the window freezes mid-build showing stale text. That exact bug had already been found once elsewhere in this editor, over the language server. | |
| 54 | + | |
| 55 | +So the view sets a flag and a ticker asks for a redraw sixty times a second while the flag is set. A dropped wake-up cannot strand anything, because the next tick is sixteen milliseconds away. | |
| 56 | + | |
| 57 | +## Windows: a pseudo-console, and why it is a file of its own | |
| 58 | + | |
| 59 | +Pseudo-terminals are the one part of this that is not portable. Linux and macOS both go through `/dev/ptmx` and differ only in which `ioctl` grants the slave. Windows has no such device: it has **pseudo-consoles** — ConPTY, since Windows 10 version 1809 — an object owned by `conhost.exe` and wired to two pipes of the editor's. What the shell prints arrives on one pipe as the same VT sequences a Unix shell writes to a pty, which is why the emulator on this side needed no Windows code at all; what the editor writes to the other pipe reaches the shell as keystrokes. | |
| 60 | + | |
| 61 | +Three things made it a file of its own rather than a variant of the Unix one. The process has to be created by hand, because attaching it to a pseudo-console takes an extended startup record that Go's `os/exec` cannot carry. The shell is `%COMSPEC%` — cmd.exe — rather than `$SHELL`, and cmd.exe reads its command line by rules of its own, so the line that runs a menu command is composed for it verbatim, the command inside one pair of quotes, rather than escaped the way every other program expects. And `conhost.exe` holds the output pipe open until the console is closed, whatever the shell does, so a goroutine waits for the shell to exit and then closes the console — that is what turns a command finishing into the end of input the window relies on to say so. Job control is cmd.exe's rather than the kernel's: `Ctrl-C` interrupts the running program as it would in a console window. | |
| 62 | + | |
| 63 | +The platform files stay split so that each platform has one honest implementation behind one small interface, and a platform with neither — the BSDs, today — gets `ErrUnsupported`, `F8` says so plainly, and nothing else in the editor is affected. | |
| 64 | + | |
| 65 | +**The Windows path has been built and vetted, not run.** turbo-core is developed on Linux and its author works on macOS. The pure parts — the environment block, the command line cmd.exe wants — are unit-tested on every platform, and the API calls compile and pass `go vet` under `GOOS=windows`; nobody has yet pressed `F8` on a Windows machine. [The how-to](../how-to/use-a-terminal.md) says what to try first. | |
| 66 | + | |
| 67 | +## How it relates to the rest | |
| 68 | + | |
| 69 | +- The exact list of what is implemented: [Terminal windows reference](../reference/terminal.md) | |
| 70 | +- Using one: [How to run shell commands without leaving the editor](../how-to/use-a-terminal.md) | |
| 71 | +- Where `terminal` sits among the packages, and why the graph runs one way: [Architecture](architecture.md) | |
| 72 | +- The dependency count this page keeps invoking: [Design decisions](design-decisions.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,72 @@ | |||
| 1 | +# Terminal windows — explanation | ||
| 2 | + | ||
| 3 | +## What is this about? | ||
| 4 | + | ||
| 5 | +`F8` opens a window with a shell in it. That sentence hides most of the work: to put a shell in a window, an editor has to become a terminal emulator, and this page is about what that involved and which of the cheaper alternatives were turned down on the way. | ||
| 6 | + | ||
| 7 | +## Why a real pseudo-terminal | ||
| 8 | + | ||
| 9 | +The obvious cheap version is to run a command with `exec.Command`, capture its output, and show it in a read-only pane. Many editors ship exactly that, and it fails on the things people actually want a terminal for. | ||
| 10 | + | ||
| 11 | +A program behaves differently when its output is a pipe rather than a terminal. Coloured output goes plain. `git log` does not page. `ls` prints one name per line. Nothing interactive works at all: no `vim`, no `ssh`, no `git rebase -i`, no answering a prompt, and no `Ctrl-C`, because with no controlling terminal there is no signal to send. For Golo the case is sharper still: `golo` with no file is a read-eval-print loop and `golo --debug` a step debugger, and both read the keyboard — neither is usable on a captured pipe, which is why the starter tools file sends them to a terminal window rather than a popup. | ||
| 12 | + | ||
| 13 | +So the shell gets a real pseudo-terminal: `/dev/ptmx` on both supported platforms, the child in a session of its own with the slave as its controlling terminal, and `TIOCSWINSZ` whenever the window is resized. That buys job control, `isatty`, `SIGWINCH` and colour, all for free, because they are the same mechanisms every other terminal uses. | ||
| 14 | + | ||
| 15 | +The cost is that the editor must then read back what a terminal is expected to understand — which is the emulator. | ||
| 16 | + | ||
| 17 | +## Why write the emulator rather than borrow one | ||
| 18 | + | ||
| 19 | +Go has terminal emulator libraries. Taking one would have meant a third dependency, against a project that has exactly two and a stated reluctance to add a third. | ||
| 20 | + | ||
| 21 | +The thing being weighed is not "emulator" against "no emulator" but against *how much* emulator. What a shell, the `golo` REPL, `git`, `less`, `htop` and `vim` need is a well-bounded list: cursor movement, the erase and insert-delete family, a scroll region, SGR in all three colour depths, the alternate screen, auto-wrap, cursor visibility and application cursor keys. That is about six hundred lines, it is written down in ECMA-48, and it is testable by writing bytes in and reading a grid out — no shell, no timing, no screen. | ||
| 22 | + | ||
| 23 | +Compare that with what a general-purpose library brings: character sets, mouse reporting protocols, sixel, bracketed paste, DEC status reports. All real, none of it needed here, and all of it surface to keep working. | ||
| 24 | + | ||
| 25 | +So the emulator is hand-written and deliberately partial, and the [reference](../reference/terminal.md) says exactly where it stops. A program that asks for something absent gets silence rather than corruption, which is the failure mode worth having: `htop` renders, `sixel` output simply does not appear. | ||
| 26 | + | ||
| 27 | +## Who gets the key press | ||
| 28 | + | ||
| 29 | +This is the decision with the most consequence for how the editor feels, and the first version got it wrong. | ||
| 30 | + | ||
| 31 | +The editor's global shortcuts are checked before the window in front sees anything. That is right for an editor and wrong the moment the window in front is a shell, because the two disagree about the same keys. `Ctrl-W` closes a window in Turbo C and deletes a word in every shell. `Ctrl-F` is Find here and forward-a-character in readline. `Ctrl-C` is copy, and also the only way to stop a runaway command. | ||
| 32 | + | ||
| 33 | +The rule chosen inverts the usual order, but only for the keys that are genuinely contested: | ||
| 34 | + | ||
| 35 | +**A focused terminal gets everything except the function keys, `Alt-X`, and `Alt-0`…`Alt-9`.** | ||
| 36 | + | ||
| 37 | +Those exceptions are not a compromise between the two claims — they are the way *out*. A full-screen program like `vim` covers the window and takes the mouse; without a reserved key there would be no way to reach the menu bar, switch windows or leave the editor short of quitting the program inside. Function keys are the natural reservation because a terminal user reaches for them least, and `Alt-X` because leaving an editor should never be in doubt. | ||
| 38 | + | ||
| 39 | +What this costs is real and worth naming: `Alt-B` and `Alt-F` reach the shell, so readline's word movement works, but a program inside a terminal window can never see `F1`…`F12`. `htop`'s function-key menu is unreachable. That is the trade, and it was made in favour of always being able to get out. | ||
| 40 | + | ||
| 41 | +## Why closing a terminal asks nothing | ||
| 42 | + | ||
| 43 | +Closing a modified file asks whether to save it. Closing a terminal does not ask anything at all, and that asymmetry is deliberate. | ||
| 44 | + | ||
| 45 | +A window with unsaved work holds something that would be *lost*. A terminal holds a running process, and closing the window is the ordinary way to say you are done with it — the same as closing a terminal emulator's tab. Asking "are you sure?" every time would train the answer out of anyone, which is the general problem with confirmations that fire on the common case. | ||
| 46 | + | ||
| 47 | +Leaving the editor closes every terminal for the same reason in reverse: a window is the only handle on those shells, so letting them outlive the editor would strand the processes with nothing able to reach them. | ||
| 48 | + | ||
| 49 | +## Why the redraws are on a clock | ||
| 50 | + | ||
| 51 | +The shell writes on a goroutine of its own; the editor draws on the main one. Waking the event loop per chunk of output looked obvious and was wrong twice over. | ||
| 52 | + | ||
| 53 | +A build writes far faster than a screen can usefully be repainted, so most of those redraws are wasted. Worse, the mechanism for waking the loop from another goroutine is tcell's `PostEvent`, which **drops** events when its queue is full — so the burst that most needs a redraw is the one whose final wake-up gets discarded, and the window freezes mid-build showing stale text. That exact bug had already been found once elsewhere in this editor, over the language server. | ||
| 54 | + | ||
| 55 | +So the view sets a flag and a ticker asks for a redraw sixty times a second while the flag is set. A dropped wake-up cannot strand anything, because the next tick is sixteen milliseconds away. | ||
| 56 | + | ||
| 57 | +## Windows: a pseudo-console, and why it is a file of its own | ||
| 58 | + | ||
| 59 | +Pseudo-terminals are the one part of this that is not portable. Linux and macOS both go through `/dev/ptmx` and differ only in which `ioctl` grants the slave. Windows has no such device: it has **pseudo-consoles** — ConPTY, since Windows 10 version 1809 — an object owned by `conhost.exe` and wired to two pipes of the editor's. What the shell prints arrives on one pipe as the same VT sequences a Unix shell writes to a pty, which is why the emulator on this side needed no Windows code at all; what the editor writes to the other pipe reaches the shell as keystrokes. | ||
| 60 | + | ||
| 61 | +Three things made it a file of its own rather than a variant of the Unix one. The process has to be created by hand, because attaching it to a pseudo-console takes an extended startup record that Go's `os/exec` cannot carry. The shell is `%COMSPEC%` — cmd.exe — rather than `$SHELL`, and cmd.exe reads its command line by rules of its own, so the line that runs a menu command is composed for it verbatim, the command inside one pair of quotes, rather than escaped the way every other program expects. And `conhost.exe` holds the output pipe open until the console is closed, whatever the shell does, so a goroutine waits for the shell to exit and then closes the console — that is what turns a command finishing into the end of input the window relies on to say so. Job control is cmd.exe's rather than the kernel's: `Ctrl-C` interrupts the running program as it would in a console window. | ||
| 62 | + | ||
| 63 | +The platform files stay split so that each platform has one honest implementation behind one small interface, and a platform with neither — the BSDs, today — gets `ErrUnsupported`, `F8` says so plainly, and nothing else in the editor is affected. | ||
| 64 | + | ||
| 65 | +**The Windows path has been built and vetted, not run.** turbo-core is developed on Linux and its author works on macOS. The pure parts — the environment block, the command line cmd.exe wants — are unit-tested on every platform, and the API calls compile and pass `go vet` under `GOOS=windows`; nobody has yet pressed `F8` on a Windows machine. [The how-to](../how-to/use-a-terminal.md) says what to try first. | ||
| 66 | + | ||
| 67 | +## How it relates to the rest | ||
| 68 | + | ||
| 69 | +- The exact list of what is implemented: [Terminal windows reference](../reference/terminal.md) | ||
| 70 | +- Using one: [How to run shell commands without leaving the editor](../how-to/use-a-terminal.md) | ||
| 71 | +- Where `terminal` sits among the packages, and why the graph runs one way: [Architecture](architecture.md) | ||
| 72 | +- The dependency count this page keeps invoking: [Design decisions](design-decisions.md) | ||
added
docs/en/how-to/ask-about-code.md +70 -0 | new file mode 100644 | ||
| @@ -0,0 +1,70 @@ | ||
| 1 | +# How to ask what the code means | |
| 2 | + | |
| 3 | +This guide shows how to follow a name through a Golo file: what it is, where it is declared, and what is wrong with it. It assumes Turbo Golo is installed and the language server is running — the status bar says `LSP: ready` when it is. | |
| 4 | + | |
| 5 | +For moving around a file — searching, jumping to a line, switching windows — see [How to move around a file](navigate-code.md) instead. | |
| 6 | + | |
| 7 | +## Put the cursor on a name | |
| 8 | + | |
| 9 | +Any character of it will do. Every question below asks about the **position of the cursor**, not about a selection, so there is nothing to highlight first. | |
| 10 | + | |
| 11 | +## Ask | |
| 12 | + | |
| 13 | +| To find | Do | Shortcut | With `golo lsp` | | |
| 14 | +| --- | --- | --- | --- | | |
| 15 | +| What it is | **Code ▸ Describe symbol** | `F1` | answered | | |
| 16 | +| Where it is declared | **Code ▸ Go to definition** | `F12` | answered | | |
| 17 | +| Where its *type* is declared | **Code ▸ Go to type definition** | | nothing found | | |
| 18 | +| What implements it | **Code ▸ Find implementations…** | | answered — its declaration | | |
| 19 | +| Everywhere it is used | **Code ▸ Find references…** | `Shift-F12` | answered, within the file | | |
| 20 | + | |
| 21 | +**One of these five reports nothing with `golo lsp`.** The server is the interpreter itself in language-server mode, and it does not advertise `typeDefinition`, so **Go to type definition** answers nothing found however good the code is. The other four work, within the file: **Find references** lists the declaration and every call, and **Find implementations** answers with the declaration — Golo has no interfaces, so a function is its own implementation. Both symbol searches below work too. See [colouring and completion](../explanation/colouring-and-completion.md) for why the one gap is written down rather than hidden behind a greyed-out menu item. | |
| 22 | + | |
| 23 | +What the two that work know is **the file in front**, at top level: | |
| 24 | + | |
| 25 | +- **Describe symbol** on a function you declared shows its signature and the `#` comments written immediately above it; on a builtin such as `println` or a symbol pulled in by `import` from an embedded module — `gololang.Errors`, `gololang.Types` — it shows the interpreter's own documentation. | |
| 26 | +- **Go to definition** takes you to a top-level function or union declared in the same file. A function imported from a module of your own on disk is not resolved. | |
| 27 | + | |
| 28 | +One answer takes you straight there. Should a question ever come back with several, they open a list showing each file, its line, and the text of that line; move with the arrow keys, `Enter` to go, `Esc` to stay where you are. | |
| 29 | + | |
| 30 | +## When nothing comes back | |
| 31 | + | |
| 32 | +Three different things look alike, and the status bar tells them apart: | |
| 33 | + | |
| 34 | +| It says | Meaning | | |
| 35 | +| --- | --- | | |
| 36 | +| `No … found`, in the question's own words | The server answered, and there are none — which, for type definitions, is what this server always says | | |
| 37 | +| `LSP: starting…` | The server has not finished starting. Wait a moment and ask again. | | |
| 38 | +| `LSP: off` on the status bar | No server is running. See [How to enable completion](enable-completion.md). | | |
| 39 | +| `LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases` | There is no `golo` on `PATH` or in `/usr/local/bin`. See [How to install GoloScript](install-goloscript.md). | | |
| 40 | + | |
| 41 | +## Find something by name instead | |
| 42 | + | |
| 43 | +- **Code ▸ Symbol in file…** lists what the file in front declares, indented, with each symbol's kind — the top-level functions and unions, each union's variants nested under it. An outline you can walk; choosing one goes to it. | |
| 44 | +- **Code ▸ Symbol in project…** (`Ctrl-T`) asks for a name and searches every `.golo` file under the project root, open or not — top-level functions, unions and module names. An empty name lists them all. | |
| 45 | + | |
| 46 | +## See what is wrong | |
| 47 | + | |
| 48 | +**Code ▸ Problems…** lists every problem the server has reported, for **every file it has loaded** — usually more than the one you are editing. Choosing one goes to the line. | |
| 49 | + | |
| 50 | +`golo lsp` reports two kinds of problem, on open and again on every edit: syntax errors from its lexer and parser, and lints for the two mistakes it sees most — `:` and `.` confused in a method call, and C-style `//` or `/* */` comments, where Golo uses `#` and `----`. A syntax error is placed on the line the message names, or on line 1 when it names none, and the whole line is marked rather than a column. | |
| 51 | + | |
| 52 | +Lines with a problem carry a mark in the gutter, beside the line number: | |
| 53 | + | |
| 54 | +| Mark | Meaning | | |
| 55 | +| --- | --- | | |
| 56 | +| `×` | An error | | |
| 57 | +| `!` | A warning | | |
| 58 | +| `i` | Information | | |
| 59 | +| `·` | A hint | | |
| 60 | + | |
| 61 | +A line with more than one problem shows the worst of them. | |
| 62 | + | |
| 63 | +**The marks need the line numbers.** They sit in the column that separates the numbers from the text, so hiding the gutter with **Options ▸ Line numbers** hides them too. | |
| 64 | + | |
| 65 | +## See also | |
| 66 | + | |
| 67 | +- Every item and its key: [Menus](../reference/menus.md) | |
| 68 | +- Getting a server running: [How to enable completion](enable-completion.md) | |
| 69 | +- What the editor asks, and why: [Colouring and completion](../explanation/colouring-and-completion.md) | |
| 70 | +- What `golo lsp` answers, exactly: [Golo tools](../reference/golo-tools.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | +# How to ask what the code means | ||
| 2 | + | ||
| 3 | +This guide shows how to follow a name through a Golo file: what it is, where it is declared, and what is wrong with it. It assumes Turbo Golo is installed and the language server is running — the status bar says `LSP: ready` when it is. | ||
| 4 | + | ||
| 5 | +For moving around a file — searching, jumping to a line, switching windows — see [How to move around a file](navigate-code.md) instead. | ||
| 6 | + | ||
| 7 | +## Put the cursor on a name | ||
| 8 | + | ||
| 9 | +Any character of it will do. Every question below asks about the **position of the cursor**, not about a selection, so there is nothing to highlight first. | ||
| 10 | + | ||
| 11 | +## Ask | ||
| 12 | + | ||
| 13 | +| To find | Do | Shortcut | With `golo lsp` | | ||
| 14 | +| --- | --- | --- | --- | | ||
| 15 | +| What it is | **Code ▸ Describe symbol** | `F1` | answered | | ||
| 16 | +| Where it is declared | **Code ▸ Go to definition** | `F12` | answered | | ||
| 17 | +| Where its *type* is declared | **Code ▸ Go to type definition** | | nothing found | | ||
| 18 | +| What implements it | **Code ▸ Find implementations…** | | answered — its declaration | | ||
| 19 | +| Everywhere it is used | **Code ▸ Find references…** | `Shift-F12` | answered, within the file | | ||
| 20 | + | ||
| 21 | +**One of these five reports nothing with `golo lsp`.** The server is the interpreter itself in language-server mode, and it does not advertise `typeDefinition`, so **Go to type definition** answers nothing found however good the code is. The other four work, within the file: **Find references** lists the declaration and every call, and **Find implementations** answers with the declaration — Golo has no interfaces, so a function is its own implementation. Both symbol searches below work too. See [colouring and completion](../explanation/colouring-and-completion.md) for why the one gap is written down rather than hidden behind a greyed-out menu item. | ||
| 22 | + | ||
| 23 | +What the two that work know is **the file in front**, at top level: | ||
| 24 | + | ||
| 25 | +- **Describe symbol** on a function you declared shows its signature and the `#` comments written immediately above it; on a builtin such as `println` or a symbol pulled in by `import` from an embedded module — `gololang.Errors`, `gololang.Types` — it shows the interpreter's own documentation. | ||
| 26 | +- **Go to definition** takes you to a top-level function or union declared in the same file. A function imported from a module of your own on disk is not resolved. | ||
| 27 | + | ||
| 28 | +One answer takes you straight there. Should a question ever come back with several, they open a list showing each file, its line, and the text of that line; move with the arrow keys, `Enter` to go, `Esc` to stay where you are. | ||
| 29 | + | ||
| 30 | +## When nothing comes back | ||
| 31 | + | ||
| 32 | +Three different things look alike, and the status bar tells them apart: | ||
| 33 | + | ||
| 34 | +| It says | Meaning | | ||
| 35 | +| --- | --- | | ||
| 36 | +| `No … found`, in the question's own words | The server answered, and there are none — which, for type definitions, is what this server always says | | ||
| 37 | +| `LSP: starting…` | The server has not finished starting. Wait a moment and ask again. | | ||
| 38 | +| `LSP: off` on the status bar | No server is running. See [How to enable completion](enable-completion.md). | | ||
| 39 | +| `LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases` | There is no `golo` on `PATH` or in `/usr/local/bin`. See [How to install GoloScript](install-goloscript.md). | | ||
| 40 | + | ||
| 41 | +## Find something by name instead | ||
| 42 | + | ||
| 43 | +- **Code ▸ Symbol in file…** lists what the file in front declares, indented, with each symbol's kind — the top-level functions and unions, each union's variants nested under it. An outline you can walk; choosing one goes to it. | ||
| 44 | +- **Code ▸ Symbol in project…** (`Ctrl-T`) asks for a name and searches every `.golo` file under the project root, open or not — top-level functions, unions and module names. An empty name lists them all. | ||
| 45 | + | ||
| 46 | +## See what is wrong | ||
| 47 | + | ||
| 48 | +**Code ▸ Problems…** lists every problem the server has reported, for **every file it has loaded** — usually more than the one you are editing. Choosing one goes to the line. | ||
| 49 | + | ||
| 50 | +`golo lsp` reports two kinds of problem, on open and again on every edit: syntax errors from its lexer and parser, and lints for the two mistakes it sees most — `:` and `.` confused in a method call, and C-style `//` or `/* */` comments, where Golo uses `#` and `----`. A syntax error is placed on the line the message names, or on line 1 when it names none, and the whole line is marked rather than a column. | ||
| 51 | + | ||
| 52 | +Lines with a problem carry a mark in the gutter, beside the line number: | ||
| 53 | + | ||
| 54 | +| Mark | Meaning | | ||
| 55 | +| --- | --- | | ||
| 56 | +| `×` | An error | | ||
| 57 | +| `!` | A warning | | ||
| 58 | +| `i` | Information | | ||
| 59 | +| `·` | A hint | | ||
| 60 | + | ||
| 61 | +A line with more than one problem shows the worst of them. | ||
| 62 | + | ||
| 63 | +**The marks need the line numbers.** They sit in the column that separates the numbers from the text, so hiding the gutter with **Options ▸ Line numbers** hides them too. | ||
| 64 | + | ||
| 65 | +## See also | ||
| 66 | + | ||
| 67 | +- Every item and its key: [Menus](../reference/menus.md) | ||
| 68 | +- Getting a server running: [How to enable completion](enable-completion.md) | ||
| 69 | +- What the editor asks, and why: [Colouring and completion](../explanation/colouring-and-completion.md) | ||
| 70 | +- What `golo lsp` answers, exactly: [Golo tools](../reference/golo-tools.md) | ||
added
docs/en/how-to/browse-a-project.md +73 -0 | new file mode 100644 | ||
| @@ -0,0 +1,73 @@ | ||
| 1 | +# How to browse a project and open files from a tree | |
| 2 | + | |
| 3 | +This guide shows how to open the project tree, walk it, and open a file from it. It assumes Turbo Golo is already installed. | |
| 4 | + | |
| 5 | +## Open the tree | |
| 6 | + | |
| 7 | +Start the editor **from the project's own directory**, then press `F9`, or choose **Window ▸ Project tree**. | |
| 8 | + | |
| 9 | +A window opens showing the project's files, named after the directory the editor was started in: | |
| 10 | + | |
| 11 | +``` | |
| 12 | +╔═[x]═══════════ turbo-golo ═══════════2═[■]╗ | |
| 13 | +║ ▶ .turbo-golo ║ | |
| 14 | +║ ▼ demos ║ | |
| 15 | +║ ▶ hello ║ | |
| 16 | +║ ▼ shapes ║ | |
| 17 | +║ shapes.golo ║ | |
| 18 | +║ shapes_test.golo ║ | |
| 19 | +║ ▶ syntax-tour ║ | |
| 20 | +║ ▶ internal ║ | |
| 21 | +║ .gitignore ║ | |
| 22 | +║ Makefile ║ | |
| 23 | +║ main.go ║ | |
| 24 | +╚═══════════════════════════════════════════╝ | |
| 25 | +``` | |
| 26 | + | |
| 27 | +Directories come first, then files, each group sorted. `.git` is the only thing hidden — `.turbo-golo`, `.gitignore` and the rest are files of your project, and you may well want to open them. | |
| 28 | + | |
| 29 | +Pressing `F9` again brings that window forward rather than opening a second tree. | |
| 30 | + | |
| 31 | +## Walk it | |
| 32 | + | |
| 33 | +| Key | Effect | | |
| 34 | +| --- | --- | | |
| 35 | +| `↑` `↓` | Move the highlight | | |
| 36 | +| `→` | Open a closed directory; on anything else, step to the next row | | |
| 37 | +| `←` | Close an open directory; on anything else, step out to the directory it is in | | |
| 38 | +| `Enter` | Open a file, or open and close a directory | | |
| 39 | +| `Home` `End` | First / last row | | |
| 40 | +| `PgUp` `PgDn` | A screenful at a time | | |
| 41 | + | |
| 42 | +A directory is read the first time you open it, so a tree on a large project costs one listing rather than a walk of everything. | |
| 43 | + | |
| 44 | +## Open a file | |
| 45 | + | |
| 46 | +Put the highlight on it and press `Enter`, or click it twice. | |
| 47 | + | |
| 48 | +The file opens in a window of its own, in front of the tree. A file that is already open is brought forward rather than opened twice. | |
| 49 | + | |
| 50 | +## See a file that was made after you opened the tree | |
| 51 | + | |
| 52 | +The tree does not watch the disk. Press **`F5`** or **`Ctrl-R`** with the tree focused, and it re-reads the project — keeping open whatever you had open, and keeping the highlight on the same entry. | |
| 53 | + | |
| 54 | +Saving a file refreshes the tree for you, so a **File ▸ Save as** into a new name shows up without asking. A file created another way — `golo new main` or `gogolo build` in a terminal window, or `git checkout` — needs the refresh key. | |
| 55 | + | |
| 56 | +## Work with the tree and a file side by side | |
| 57 | + | |
| 58 | +The tree is an ordinary window, so all the window commands apply: | |
| 59 | + | |
| 60 | +- **Window ▸ Tile** puts the tree and your file side by side. | |
| 61 | +- Drag its bottom-right corner to make it narrower once you know your way around. | |
| 62 | +- `[x]` closes it; `F9` brings it back. | |
| 63 | + | |
| 64 | +## Variants | |
| 65 | + | |
| 66 | +- **You started the editor from a subdirectory.** The tree is rooted there, showing only that part of the project. Start from the project's own directory instead — the same rule `.turbo-golo/settings.toml` follows. | |
| 67 | +- **A directory shows as open but empty.** It could not be read, most often a permissions problem. The rest of the tree is unaffected; fix the permissions and press `F5`. | |
| 68 | + | |
| 69 | +## See also | |
| 70 | + | |
| 71 | +- Every key and what the tree shows, exactly: [Project tree reference](../reference/project-tree.md) | |
| 72 | +- Why it is a window rather than a docked panel, and why it does not watch the disk: [Project tree](../explanation/project-tree.md) | |
| 73 | +- Colouring it: [Theme file format](../reference/themes.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,73 @@ | |||
| 1 | +# How to browse a project and open files from a tree | ||
| 2 | + | ||
| 3 | +This guide shows how to open the project tree, walk it, and open a file from it. It assumes Turbo Golo is already installed. | ||
| 4 | + | ||
| 5 | +## Open the tree | ||
| 6 | + | ||
| 7 | +Start the editor **from the project's own directory**, then press `F9`, or choose **Window ▸ Project tree**. | ||
| 8 | + | ||
| 9 | +A window opens showing the project's files, named after the directory the editor was started in: | ||
| 10 | + | ||
| 11 | +``` | ||
| 12 | +╔═[x]═══════════ turbo-golo ═══════════2═[■]╗ | ||
| 13 | +║ ▶ .turbo-golo ║ | ||
| 14 | +║ ▼ demos ║ | ||
| 15 | +║ ▶ hello ║ | ||
| 16 | +║ ▼ shapes ║ | ||
| 17 | +║ shapes.golo ║ | ||
| 18 | +║ shapes_test.golo ║ | ||
| 19 | +║ ▶ syntax-tour ║ | ||
| 20 | +║ ▶ internal ║ | ||
| 21 | +║ .gitignore ║ | ||
| 22 | +║ Makefile ║ | ||
| 23 | +║ main.go ║ | ||
| 24 | +╚═══════════════════════════════════════════╝ | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +Directories come first, then files, each group sorted. `.git` is the only thing hidden — `.turbo-golo`, `.gitignore` and the rest are files of your project, and you may well want to open them. | ||
| 28 | + | ||
| 29 | +Pressing `F9` again brings that window forward rather than opening a second tree. | ||
| 30 | + | ||
| 31 | +## Walk it | ||
| 32 | + | ||
| 33 | +| Key | Effect | | ||
| 34 | +| --- | --- | | ||
| 35 | +| `↑` `↓` | Move the highlight | | ||
| 36 | +| `→` | Open a closed directory; on anything else, step to the next row | | ||
| 37 | +| `←` | Close an open directory; on anything else, step out to the directory it is in | | ||
| 38 | +| `Enter` | Open a file, or open and close a directory | | ||
| 39 | +| `Home` `End` | First / last row | | ||
| 40 | +| `PgUp` `PgDn` | A screenful at a time | | ||
| 41 | + | ||
| 42 | +A directory is read the first time you open it, so a tree on a large project costs one listing rather than a walk of everything. | ||
| 43 | + | ||
| 44 | +## Open a file | ||
| 45 | + | ||
| 46 | +Put the highlight on it and press `Enter`, or click it twice. | ||
| 47 | + | ||
| 48 | +The file opens in a window of its own, in front of the tree. A file that is already open is brought forward rather than opened twice. | ||
| 49 | + | ||
| 50 | +## See a file that was made after you opened the tree | ||
| 51 | + | ||
| 52 | +The tree does not watch the disk. Press **`F5`** or **`Ctrl-R`** with the tree focused, and it re-reads the project — keeping open whatever you had open, and keeping the highlight on the same entry. | ||
| 53 | + | ||
| 54 | +Saving a file refreshes the tree for you, so a **File ▸ Save as** into a new name shows up without asking. A file created another way — `golo new main` or `gogolo build` in a terminal window, or `git checkout` — needs the refresh key. | ||
| 55 | + | ||
| 56 | +## Work with the tree and a file side by side | ||
| 57 | + | ||
| 58 | +The tree is an ordinary window, so all the window commands apply: | ||
| 59 | + | ||
| 60 | +- **Window ▸ Tile** puts the tree and your file side by side. | ||
| 61 | +- Drag its bottom-right corner to make it narrower once you know your way around. | ||
| 62 | +- `[x]` closes it; `F9` brings it back. | ||
| 63 | + | ||
| 64 | +## Variants | ||
| 65 | + | ||
| 66 | +- **You started the editor from a subdirectory.** The tree is rooted there, showing only that part of the project. Start from the project's own directory instead — the same rule `.turbo-golo/settings.toml` follows. | ||
| 67 | +- **A directory shows as open but empty.** It could not be read, most often a permissions problem. The rest of the tree is unaffected; fix the permissions and press `F5`. | ||
| 68 | + | ||
| 69 | +## See also | ||
| 70 | + | ||
| 71 | +- Every key and what the tree shows, exactly: [Project tree reference](../reference/project-tree.md) | ||
| 72 | +- Why it is a window rather than a docked panel, and why it does not watch the disk: [Project tree](../explanation/project-tree.md) | ||
| 73 | +- Colouring it: [Theme file format](../reference/themes.md) | ||
added
docs/en/how-to/configure-a-project.md +83 -0 | new file mode 100644 | ||
| @@ -0,0 +1,83 @@ | ||
| 1 | +# How to give a project its own settings | |
| 2 | + | |
| 3 | +This guide shows how to pin a theme and turn on automatic saving for one project, so everyone who opens it gets the same editor. It assumes you already have Turbo Golo installed. | |
| 4 | + | |
| 5 | +## Create the settings file | |
| 6 | + | |
| 7 | +Start the editor **from the project's own directory**, then choose **Options ▸ Create project settings**. | |
| 8 | + | |
| 9 | +That writes `.turbo-golo/settings.toml`, filled in with the theme you are using right now, and opens it for editing — coloured, because Turbo Golo colours TOML: | |
| 10 | + | |
| 11 | +```toml | |
| 12 | +# turbo-golo project settings. | |
| 13 | +# | |
| 14 | +# These apply to everyone who opens this project in turbo-golo. Delete this | |
| 15 | +# file and the editor falls back to its own defaults. | |
| 16 | + | |
| 17 | +[editor] | |
| 18 | + | |
| 19 | +# The colour theme to start in. `turbo-golo -list-themes` lists them all. | |
| 20 | +# A -theme flag on the command line overrides this. | |
| 21 | +theme = "turbo-classic" | |
| 22 | + | |
| 23 | +# Write modified files by themselves, a short while after you stop typing. | |
| 24 | +# On, because a project that has gone to the trouble of having a settings file | |
| 25 | +# has said what it wants; set it to false and save, and it stops at once. | |
| 26 | +autosave = true | |
| 27 | + | |
| 28 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | |
| 29 | +autosave_delay = "2s" | |
| 30 | +``` | |
| 31 | + | |
| 32 | +The file is read when the editor starts, and **again every time you save it** — so a change is in force the moment you press `F2`. The status bar confirms it: `Applied .turbo-golo/settings.toml — autosave on (2s)`. | |
| 33 | + | |
| 34 | +That covers the settings this file holds, not the theme: **Options ▸ Theme** is the live way to change that, and writes your choice back here for you. | |
| 35 | + | |
| 36 | +The menu item you just used is now greyed out, and **Options ▸ Project settings…** beside it is not. That is the rule for all three of the project's files: you can create the one you have not got, and open the one you have. | |
| 37 | + | |
| 38 | +## Automatic saving | |
| 39 | + | |
| 40 | +It is already on: the file you were just given says `autosave = true`. | |
| 41 | + | |
| 42 | +Any file with a name is written two seconds after you stop typing. The status bar says `Saved main.golo` when it happens. Nothing is written while you are still typing — each keystroke pushes the wait out again. | |
| 43 | + | |
| 44 | +Two things change as a consequence, both on purpose: | |
| 45 | + | |
| 46 | +- **Closing a window stops asking** whether to save. It was going to be saved anyway. | |
| 47 | +- **Leaving the editor stops asking** too, for the same reason. | |
| 48 | + | |
| 49 | +A file that has never been named is the exception: automatic saving never opens a dialog, so an untitled window keeps its `*` and is still asked about when you close it. | |
| 50 | + | |
| 51 | +To turn it off, set `autosave` to `false` and save; the status bar answers `autosave off`, and it stops there and then. To wait longer or less, change `autosave_delay`: | |
| 52 | + | |
| 53 | +```toml | |
| 54 | +autosave_delay = "500ms" | |
| 55 | +``` | |
| 56 | + | |
| 57 | +## Pin the theme | |
| 58 | + | |
| 59 | +Set `theme` to any name from `turbo-golo -list-themes`, or simply pick one with **Options ▸ Theme** — with a settings file present, choosing a theme writes it into the file for you, keeping your comments and layout as they were. | |
| 60 | + | |
| 61 | +## Try a different theme without changing the file | |
| 62 | + | |
| 63 | +Pass `-theme` on the command line. It wins over the project's choice for that run only: | |
| 64 | + | |
| 65 | +```bash | |
| 66 | +turbo-golo -theme turbo-dark main.golo | |
| 67 | +``` | |
| 68 | + | |
| 69 | +## Edit the file later | |
| 70 | + | |
| 71 | +**Options ▸ Project settings…** opens it again. It is greyed out in a project that has none. | |
| 72 | + | |
| 73 | +## Variants | |
| 74 | + | |
| 75 | +- **You start the editor from a subdirectory.** The settings are not found: only `./.turbo-golo` is looked at, with no walk up towards the project root. Start from the project's own directory, or pass `-theme` for that run. | |
| 76 | +- **The file has a mistake in it.** The editor says so on standard error and opens with its defaults, so you can fix the file in the editor itself. | |
| 77 | +- **You share the project.** `.turbo-golo/settings.toml` is an ordinary file; commit it to agree on a theme across a team, or add it to `.gitignore` to keep it to yourself. | |
| 78 | + | |
| 79 | +## See also | |
| 80 | + | |
| 81 | +- Every key, with its type and default: [Project settings reference](../reference/project-settings.md) | |
| 82 | +- Why the file is not searched for in parent directories, and why autosave waits: [Project settings](../explanation/project-settings.md) | |
| 83 | +- Writing a theme to pin: [How to write your own theme](write-a-theme.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | +# How to give a project its own settings | ||
| 2 | + | ||
| 3 | +This guide shows how to pin a theme and turn on automatic saving for one project, so everyone who opens it gets the same editor. It assumes you already have Turbo Golo installed. | ||
| 4 | + | ||
| 5 | +## Create the settings file | ||
| 6 | + | ||
| 7 | +Start the editor **from the project's own directory**, then choose **Options ▸ Create project settings**. | ||
| 8 | + | ||
| 9 | +That writes `.turbo-golo/settings.toml`, filled in with the theme you are using right now, and opens it for editing — coloured, because Turbo Golo colours TOML: | ||
| 10 | + | ||
| 11 | +```toml | ||
| 12 | +# turbo-golo project settings. | ||
| 13 | +# | ||
| 14 | +# These apply to everyone who opens this project in turbo-golo. Delete this | ||
| 15 | +# file and the editor falls back to its own defaults. | ||
| 16 | + | ||
| 17 | +[editor] | ||
| 18 | + | ||
| 19 | +# The colour theme to start in. `turbo-golo -list-themes` lists them all. | ||
| 20 | +# A -theme flag on the command line overrides this. | ||
| 21 | +theme = "turbo-classic" | ||
| 22 | + | ||
| 23 | +# Write modified files by themselves, a short while after you stop typing. | ||
| 24 | +# On, because a project that has gone to the trouble of having a settings file | ||
| 25 | +# has said what it wants; set it to false and save, and it stops at once. | ||
| 26 | +autosave = true | ||
| 27 | + | ||
| 28 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | ||
| 29 | +autosave_delay = "2s" | ||
| 30 | +``` | ||
| 31 | + | ||
| 32 | +The file is read when the editor starts, and **again every time you save it** — so a change is in force the moment you press `F2`. The status bar confirms it: `Applied .turbo-golo/settings.toml — autosave on (2s)`. | ||
| 33 | + | ||
| 34 | +That covers the settings this file holds, not the theme: **Options ▸ Theme** is the live way to change that, and writes your choice back here for you. | ||
| 35 | + | ||
| 36 | +The menu item you just used is now greyed out, and **Options ▸ Project settings…** beside it is not. That is the rule for all three of the project's files: you can create the one you have not got, and open the one you have. | ||
| 37 | + | ||
| 38 | +## Automatic saving | ||
| 39 | + | ||
| 40 | +It is already on: the file you were just given says `autosave = true`. | ||
| 41 | + | ||
| 42 | +Any file with a name is written two seconds after you stop typing. The status bar says `Saved main.golo` when it happens. Nothing is written while you are still typing — each keystroke pushes the wait out again. | ||
| 43 | + | ||
| 44 | +Two things change as a consequence, both on purpose: | ||
| 45 | + | ||
| 46 | +- **Closing a window stops asking** whether to save. It was going to be saved anyway. | ||
| 47 | +- **Leaving the editor stops asking** too, for the same reason. | ||
| 48 | + | ||
| 49 | +A file that has never been named is the exception: automatic saving never opens a dialog, so an untitled window keeps its `*` and is still asked about when you close it. | ||
| 50 | + | ||
| 51 | +To turn it off, set `autosave` to `false` and save; the status bar answers `autosave off`, and it stops there and then. To wait longer or less, change `autosave_delay`: | ||
| 52 | + | ||
| 53 | +```toml | ||
| 54 | +autosave_delay = "500ms" | ||
| 55 | +``` | ||
| 56 | + | ||
| 57 | +## Pin the theme | ||
| 58 | + | ||
| 59 | +Set `theme` to any name from `turbo-golo -list-themes`, or simply pick one with **Options ▸ Theme** — with a settings file present, choosing a theme writes it into the file for you, keeping your comments and layout as they were. | ||
| 60 | + | ||
| 61 | +## Try a different theme without changing the file | ||
| 62 | + | ||
| 63 | +Pass `-theme` on the command line. It wins over the project's choice for that run only: | ||
| 64 | + | ||
| 65 | +```bash | ||
| 66 | +turbo-golo -theme turbo-dark main.golo | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +## Edit the file later | ||
| 70 | + | ||
| 71 | +**Options ▸ Project settings…** opens it again. It is greyed out in a project that has none. | ||
| 72 | + | ||
| 73 | +## Variants | ||
| 74 | + | ||
| 75 | +- **You start the editor from a subdirectory.** The settings are not found: only `./.turbo-golo` is looked at, with no walk up towards the project root. Start from the project's own directory, or pass `-theme` for that run. | ||
| 76 | +- **The file has a mistake in it.** The editor says so on standard error and opens with its defaults, so you can fix the file in the editor itself. | ||
| 77 | +- **You share the project.** `.turbo-golo/settings.toml` is an ordinary file; commit it to agree on a theme across a team, or add it to `.gitignore` to keep it to yourself. | ||
| 78 | + | ||
| 79 | +## See also | ||
| 80 | + | ||
| 81 | +- Every key, with its type and default: [Project settings reference](../reference/project-settings.md) | ||
| 82 | +- Why the file is not searched for in parent directories, and why autosave waits: [Project settings](../explanation/project-settings.md) | ||
| 83 | +- Writing a theme to pin: [How to write your own theme](write-a-theme.md) | ||
added
docs/en/how-to/enable-completion.md +137 -0 | new file mode 100644 | ||
| @@ -0,0 +1,137 @@ | ||
| 1 | +# How to enable Golo completion | |
| 2 | + | |
| 3 | +This guide shows how to get completion, hovers, go-to-definition and error marks working. It assumes Turbo Golo is already installed. | |
| 4 | + | |
| 5 | +Completion comes from **`golo lsp`** — the GoloScript interpreter itself, started in language-server mode. There is no separate server to install: a machine that can run a Golo script can complete one. Turbo Golo does not bundle the interpreter, though: editing and colouring work without it, and only completion and the error marks are lost. | |
| 6 | + | |
| 7 | +## 1. Install golo | |
| 8 | + | |
| 9 | +Either download a binary from the [releases page](https://codeberg.org/TypeUnsafe/golo-script/releases): | |
| 10 | + | |
| 11 | +```bash | |
| 12 | +chmod +x golo-<version>-<platform> | |
| 13 | +sudo mv golo-<version>-<platform> /usr/local/bin/golo | |
| 14 | +golo --version | |
| 15 | +``` | |
| 16 | + | |
| 17 | +or build it from source, which also gives you the two compilers: | |
| 18 | + | |
| 19 | +```bash | |
| 20 | +git clone https://codeberg.org/TypeUnsafe/golo-script.git && cd golo-script | |
| 21 | +./install.sh | |
| 22 | +``` | |
| 23 | + | |
| 24 | +Turbo Golo's own installer will do the second for you: `scripts/install.sh --with-server`. [How to install GoloScript](install-goloscript.md) has the details. | |
| 25 | + | |
| 26 | +## 2. Make sure Turbo Golo can find it | |
| 27 | + | |
| 28 | +Turbo Golo looks on `PATH` first, then in `/usr/local/bin`, which is where GoloScript's installer writes. Check: | |
| 29 | + | |
| 30 | +```bash | |
| 31 | +golo --version | |
| 32 | +``` | |
| 33 | + | |
| 34 | +``` | |
| 35 | +v0.1.1 | dev.20260802.🤓 | |
| 36 | +``` | |
| 37 | + | |
| 38 | +If that says "command not found" but Turbo Golo still finds it, that is expected and fine: the editor searched `/usr/local/bin` itself. | |
| 39 | + | |
| 40 | +## 3. Open a script | |
| 41 | + | |
| 42 | +```bash | |
| 43 | +cd /path/to/your/scripts | |
| 44 | +turbo-golo main.golo | |
| 45 | +``` | |
| 46 | + | |
| 47 | +Golo has no project manifest, so there is nothing to look for: `golo lsp` is started in the directory of the file you opened, and it answers about that file. Where you start the editor changes nothing about completion — it decides where the Golo menu's commands run, which is a different matter. | |
| 48 | + | |
| 49 | +## 4. Ask for a completion | |
| 50 | + | |
| 51 | +Type the first letters of a name and press **Ctrl-Space**: | |
| 52 | + | |
| 53 | +```golo | |
| 54 | +prin | |
| 55 | +``` | |
| 56 | + | |
| 57 | +A list drops down under the cursor — `print`, `println`, with their signatures. Keep typing to narrow it, **↑ ↓** to walk it, **Enter** or **Tab** to accept, **Escape** to dismiss. | |
| 58 | + | |
| 59 | +## What the list holds | |
| 60 | + | |
| 61 | +`golo lsp` offers four kinds of thing: | |
| 62 | + | |
| 63 | +| Offered | Example | | |
| 64 | +| --- | --- | | |
| 65 | +| Keywords | `function`, `foreach`, `augment` | | |
| 66 | +| The interpreter's builtins, with their signatures and documentation | `println`, `readFile`, `httpGet`, `DynamicObject` | | |
| 67 | +| Functions and unions declared **at top level** in the file | your own `function helper = …` | | |
| 68 | +| Symbols pulled in by `import` from the modules embedded in the binary | `Some`, `None`, `isSome`, `either` after `import gololang.Errors` | | |
| 69 | + | |
| 70 | +Two things are deliberately **not** in it, and both look like a broken server if you did not know: | |
| 71 | + | |
| 72 | +- **A function declared inside another function.** Only top-level declarations are collected. Move it out, or accept that it will not be offered. | |
| 73 | +- **Anything from a `.golo` file of your own.** `import` resolves the modules built into the interpreter — `gololang.Errors`, `gololang.Types`, `gololang.Ui`, `gololang.Testing`, … — and nothing on disk. A helper in `lib/util.golo` is not seen from `main.golo`. | |
| 74 | + | |
| 75 | +## Checking what the server is doing | |
| 76 | + | |
| 77 | +The right-hand end of the status bar shows the language server's state: `LSP: starting…`, `LSP: ready`, or why there is none: | |
| 78 | + | |
| 79 | +``` | |
| 80 | +LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases | |
| 81 | +``` | |
| 82 | + | |
| 83 | +`Run ▸ Language server status` shows the same thing in a box, with the path it found the binary at and the directory it started it in. | |
| 84 | + | |
| 85 | +## Variants | |
| 86 | + | |
| 87 | +**You do not want a language server at all:** | |
| 88 | + | |
| 89 | +```bash | |
| 90 | +turbo-golo -no-lsp main.golo | |
| 91 | +``` | |
| 92 | + | |
| 93 | +**Completion is dead in a window that started without a name.** An Untitled window has no file to announce to `golo lsp` until it is saved — press **F2** and give it a name ending in `.golo`. From that save on, completion, hover and the error marks work in that window; there is no need to quit and relaunch. | |
| 94 | + | |
| 95 | +**The list is empty.** `golo lsp` answers for any file, including one that does not parse — it lists keywords and builtins regardless — so an empty list almost always means the server is not running. Read the status bar. | |
| 96 | + | |
| 97 | +**Ctrl-Space does nothing.** tmux, screen and IDE terminals frequently claim `Ctrl-Space` before the editor sees it. Use `Run ▸ Completion` instead. | |
| 98 | + | |
| 99 | +**A request takes too long.** Every request gives up after a few seconds, so a stuck server slows the editor but never freezes it. The status bar reports the failure. | |
| 100 | + | |
| 101 | +**You installed golo somewhere unusual.** The editor searches `PATH` and `/usr/local/bin`, and nowhere else — there is no environment variable naming another directory. Put the directory on `PATH`, or a symbolic link in `/usr/local/bin`. | |
| 102 | + | |
| 103 | +## What else the server gives you | |
| 104 | + | |
| 105 | +Completion is the loudest thing it does and the least of what it knows. The same connection answers four more questions, all of them in the **Code** menu — three about the symbol under the cursor, no selection needed, and one about a name you type. | |
| 106 | + | |
| 107 | +| Key | What it does | With `golo lsp` | | |
| 108 | +| --- | --- | --- | | |
| 109 | +| **Ctrl-Space** | Completion list | yes | | |
| 110 | +| **F1** | Describe the symbol under the cursor | yes — for a function you declared, the `#` comments written just above it; for a builtin, its signature and a worked example | | |
| 111 | +| **F12** | Jump to where it is declared | yes, within the file | | |
| 112 | +| **Shift-F12** | List everywhere it is used | yes, within the file: its declaration and every call | | |
| 113 | +| **Ctrl-T** | Find a symbol by name anywhere in the project | yes: the top-level functions, unions and module names of every `.golo` file under the project root, open or not | | |
| 114 | + | |
| 115 | +And, without a key: *Symbol in file…* lists the file's top-level functions and unions, with each union's variants nested under it; *Problems…* lists every diagnostic; *Find implementations…* answers with the function's declaration, the same place **F12** goes — Golo has no interfaces, so a function is its own implementation; *Go to type definition* reports nothing found. | |
| 116 | + | |
| 117 | +The one that reports nothing is the server's boundary, not the editor's: `golo lsp` advertises `completion`, `hover`, `definition`, `documentSymbol`, `references`, `implementation` and `workspaceSymbol`, and not `typeDefinition`. Until GoloScript v0.2.0 it advertised only the first four and this table said so; a test in this repository failed the day the server started answering the other three, which is how the table came to be revised. The menu item stays because greying it out depending on what a server said at start-up would make the menu a different shape on different machines, and the same kind of test still fails the day a future golo answers type definitions too. | |
| 118 | + | |
| 119 | +## The error marks | |
| 120 | + | |
| 121 | +Problems the server finds arrive unasked, on open and on every edit. The first error in the file you are editing appears on the right of the status bar, prefixed with `⚠`; every line with a problem gets a `×` in the gutter; and **Code ▸ Problems…** lists all of them. | |
| 122 | + | |
| 123 | +`golo lsp` reports three kinds: | |
| 124 | + | |
| 125 | +- **Syntax errors** from the lexer and parser — a missing brace, a token the parser does not accept. The parser's messages carry a line and no column, so the mark lands on the whole line; a message with no line at all lands on line 1. | |
| 126 | +- **A `:`/`.` confusion** — `obj.method()` where Golo wants `obj: method()`. | |
| 127 | +- **A C-style comment** — `//` or `/* */`, which Golo does not have. Golo comments are `#` and `----`. | |
| 128 | + | |
| 129 | +A script that parses and then fails when run gets no mark: the server parses, it never runs anything. | |
| 130 | + | |
| 131 | +[How to ask what the code means](ask-about-code.md) walks through the Code menu. | |
| 132 | + | |
| 133 | +## See also | |
| 134 | + | |
| 135 | +- Why the server is optional, and why the interpreter is the server: [Colouring and completion](../explanation/colouring-and-completion.md) | |
| 136 | +- Installing the interpreter: [How to install GoloScript](install-goloscript.md) | |
| 137 | +- Every key: [keyboard reference](../reference/keyboard.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,137 @@ | |||
| 1 | +# How to enable Golo completion | ||
| 2 | + | ||
| 3 | +This guide shows how to get completion, hovers, go-to-definition and error marks working. It assumes Turbo Golo is already installed. | ||
| 4 | + | ||
| 5 | +Completion comes from **`golo lsp`** — the GoloScript interpreter itself, started in language-server mode. There is no separate server to install: a machine that can run a Golo script can complete one. Turbo Golo does not bundle the interpreter, though: editing and colouring work without it, and only completion and the error marks are lost. | ||
| 6 | + | ||
| 7 | +## 1. Install golo | ||
| 8 | + | ||
| 9 | +Either download a binary from the [releases page](https://codeberg.org/TypeUnsafe/golo-script/releases): | ||
| 10 | + | ||
| 11 | +```bash | ||
| 12 | +chmod +x golo-<version>-<platform> | ||
| 13 | +sudo mv golo-<version>-<platform> /usr/local/bin/golo | ||
| 14 | +golo --version | ||
| 15 | +``` | ||
| 16 | + | ||
| 17 | +or build it from source, which also gives you the two compilers: | ||
| 18 | + | ||
| 19 | +```bash | ||
| 20 | +git clone https://codeberg.org/TypeUnsafe/golo-script.git && cd golo-script | ||
| 21 | +./install.sh | ||
| 22 | +``` | ||
| 23 | + | ||
| 24 | +Turbo Golo's own installer will do the second for you: `scripts/install.sh --with-server`. [How to install GoloScript](install-goloscript.md) has the details. | ||
| 25 | + | ||
| 26 | +## 2. Make sure Turbo Golo can find it | ||
| 27 | + | ||
| 28 | +Turbo Golo looks on `PATH` first, then in `/usr/local/bin`, which is where GoloScript's installer writes. Check: | ||
| 29 | + | ||
| 30 | +```bash | ||
| 31 | +golo --version | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +``` | ||
| 35 | +v0.1.1 | dev.20260802.🤓 | ||
| 36 | +``` | ||
| 37 | + | ||
| 38 | +If that says "command not found" but Turbo Golo still finds it, that is expected and fine: the editor searched `/usr/local/bin` itself. | ||
| 39 | + | ||
| 40 | +## 3. Open a script | ||
| 41 | + | ||
| 42 | +```bash | ||
| 43 | +cd /path/to/your/scripts | ||
| 44 | +turbo-golo main.golo | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +Golo has no project manifest, so there is nothing to look for: `golo lsp` is started in the directory of the file you opened, and it answers about that file. Where you start the editor changes nothing about completion — it decides where the Golo menu's commands run, which is a different matter. | ||
| 48 | + | ||
| 49 | +## 4. Ask for a completion | ||
| 50 | + | ||
| 51 | +Type the first letters of a name and press **Ctrl-Space**: | ||
| 52 | + | ||
| 53 | +```golo | ||
| 54 | +prin | ||
| 55 | +``` | ||
| 56 | + | ||
| 57 | +A list drops down under the cursor — `print`, `println`, with their signatures. Keep typing to narrow it, **↑ ↓** to walk it, **Enter** or **Tab** to accept, **Escape** to dismiss. | ||
| 58 | + | ||
| 59 | +## What the list holds | ||
| 60 | + | ||
| 61 | +`golo lsp` offers four kinds of thing: | ||
| 62 | + | ||
| 63 | +| Offered | Example | | ||
| 64 | +| --- | --- | | ||
| 65 | +| Keywords | `function`, `foreach`, `augment` | | ||
| 66 | +| The interpreter's builtins, with their signatures and documentation | `println`, `readFile`, `httpGet`, `DynamicObject` | | ||
| 67 | +| Functions and unions declared **at top level** in the file | your own `function helper = …` | | ||
| 68 | +| Symbols pulled in by `import` from the modules embedded in the binary | `Some`, `None`, `isSome`, `either` after `import gololang.Errors` | | ||
| 69 | + | ||
| 70 | +Two things are deliberately **not** in it, and both look like a broken server if you did not know: | ||
| 71 | + | ||
| 72 | +- **A function declared inside another function.** Only top-level declarations are collected. Move it out, or accept that it will not be offered. | ||
| 73 | +- **Anything from a `.golo` file of your own.** `import` resolves the modules built into the interpreter — `gololang.Errors`, `gololang.Types`, `gololang.Ui`, `gololang.Testing`, … — and nothing on disk. A helper in `lib/util.golo` is not seen from `main.golo`. | ||
| 74 | + | ||
| 75 | +## Checking what the server is doing | ||
| 76 | + | ||
| 77 | +The right-hand end of the status bar shows the language server's state: `LSP: starting…`, `LSP: ready`, or why there is none: | ||
| 78 | + | ||
| 79 | +``` | ||
| 80 | +LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases | ||
| 81 | +``` | ||
| 82 | + | ||
| 83 | +`Run ▸ Language server status` shows the same thing in a box, with the path it found the binary at and the directory it started it in. | ||
| 84 | + | ||
| 85 | +## Variants | ||
| 86 | + | ||
| 87 | +**You do not want a language server at all:** | ||
| 88 | + | ||
| 89 | +```bash | ||
| 90 | +turbo-golo -no-lsp main.golo | ||
| 91 | +``` | ||
| 92 | + | ||
| 93 | +**Completion is dead in a window that started without a name.** An Untitled window has no file to announce to `golo lsp` until it is saved — press **F2** and give it a name ending in `.golo`. From that save on, completion, hover and the error marks work in that window; there is no need to quit and relaunch. | ||
| 94 | + | ||
| 95 | +**The list is empty.** `golo lsp` answers for any file, including one that does not parse — it lists keywords and builtins regardless — so an empty list almost always means the server is not running. Read the status bar. | ||
| 96 | + | ||
| 97 | +**Ctrl-Space does nothing.** tmux, screen and IDE terminals frequently claim `Ctrl-Space` before the editor sees it. Use `Run ▸ Completion` instead. | ||
| 98 | + | ||
| 99 | +**A request takes too long.** Every request gives up after a few seconds, so a stuck server slows the editor but never freezes it. The status bar reports the failure. | ||
| 100 | + | ||
| 101 | +**You installed golo somewhere unusual.** The editor searches `PATH` and `/usr/local/bin`, and nowhere else — there is no environment variable naming another directory. Put the directory on `PATH`, or a symbolic link in `/usr/local/bin`. | ||
| 102 | + | ||
| 103 | +## What else the server gives you | ||
| 104 | + | ||
| 105 | +Completion is the loudest thing it does and the least of what it knows. The same connection answers four more questions, all of them in the **Code** menu — three about the symbol under the cursor, no selection needed, and one about a name you type. | ||
| 106 | + | ||
| 107 | +| Key | What it does | With `golo lsp` | | ||
| 108 | +| --- | --- | --- | | ||
| 109 | +| **Ctrl-Space** | Completion list | yes | | ||
| 110 | +| **F1** | Describe the symbol under the cursor | yes — for a function you declared, the `#` comments written just above it; for a builtin, its signature and a worked example | | ||
| 111 | +| **F12** | Jump to where it is declared | yes, within the file | | ||
| 112 | +| **Shift-F12** | List everywhere it is used | yes, within the file: its declaration and every call | | ||
| 113 | +| **Ctrl-T** | Find a symbol by name anywhere in the project | yes: the top-level functions, unions and module names of every `.golo` file under the project root, open or not | | ||
| 114 | + | ||
| 115 | +And, without a key: *Symbol in file…* lists the file's top-level functions and unions, with each union's variants nested under it; *Problems…* lists every diagnostic; *Find implementations…* answers with the function's declaration, the same place **F12** goes — Golo has no interfaces, so a function is its own implementation; *Go to type definition* reports nothing found. | ||
| 116 | + | ||
| 117 | +The one that reports nothing is the server's boundary, not the editor's: `golo lsp` advertises `completion`, `hover`, `definition`, `documentSymbol`, `references`, `implementation` and `workspaceSymbol`, and not `typeDefinition`. Until GoloScript v0.2.0 it advertised only the first four and this table said so; a test in this repository failed the day the server started answering the other three, which is how the table came to be revised. The menu item stays because greying it out depending on what a server said at start-up would make the menu a different shape on different machines, and the same kind of test still fails the day a future golo answers type definitions too. | ||
| 118 | + | ||
| 119 | +## The error marks | ||
| 120 | + | ||
| 121 | +Problems the server finds arrive unasked, on open and on every edit. The first error in the file you are editing appears on the right of the status bar, prefixed with `⚠`; every line with a problem gets a `×` in the gutter; and **Code ▸ Problems…** lists all of them. | ||
| 122 | + | ||
| 123 | +`golo lsp` reports three kinds: | ||
| 124 | + | ||
| 125 | +- **Syntax errors** from the lexer and parser — a missing brace, a token the parser does not accept. The parser's messages carry a line and no column, so the mark lands on the whole line; a message with no line at all lands on line 1. | ||
| 126 | +- **A `:`/`.` confusion** — `obj.method()` where Golo wants `obj: method()`. | ||
| 127 | +- **A C-style comment** — `//` or `/* */`, which Golo does not have. Golo comments are `#` and `----`. | ||
| 128 | + | ||
| 129 | +A script that parses and then fails when run gets no mark: the server parses, it never runs anything. | ||
| 130 | + | ||
| 131 | +[How to ask what the code means](ask-about-code.md) walks through the Code menu. | ||
| 132 | + | ||
| 133 | +## See also | ||
| 134 | + | ||
| 135 | +- Why the server is optional, and why the interpreter is the server: [Colouring and completion](../explanation/colouring-and-completion.md) | ||
| 136 | +- Installing the interpreter: [How to install GoloScript](install-goloscript.md) | ||
| 137 | +- Every key: [keyboard reference](../reference/keyboard.md) | ||
added
docs/en/how-to/install-goloscript.md +100 -0 | new file mode 100644 | ||
| @@ -0,0 +1,100 @@ | ||
| 1 | +# How to install GoloScript | |
| 2 | + | |
| 3 | +This guide shows how to get `golo` — and, if you want them, `gogolo` and `wagolo` — onto a machine, and how to check that Turbo Golo can find it. It assumes you already have Turbo Golo, or are about to — see [how to install the editor](install.md) for that. | |
| 4 | + | |
| 5 | +**The editor works without any of this.** Editing, colouring, themes, snippets and terminal windows all run with no interpreter at all. What needs it is completion, the error marks in the gutter, and every command in the Golo menu. | |
| 6 | + | |
| 7 | +## Which binary you need | |
| 8 | + | |
| 9 | +GoloScript ships three, under one version: | |
| 10 | + | |
| 11 | +| Binary | What it is for | Also needs | | |
| 12 | +| --- | --- | --- | | |
| 13 | +| `golo` | The interpreter: runs scripts, and is also the REPL, the debugger, the test runner and **the language server** | nothing | | |
| 14 | +| `gogolo` | Compiles a script to a native executable, through Go | the Go toolchain | | |
| 15 | +| `wagolo` | Compiles a script to WebAssembly, through Go and TinyGo | TinyGo, and `wasm-tools` for the `wasip2` target | | |
| 16 | + | |
| 17 | +The editor needs `golo` and nothing else. `gogolo` and `wagolo` are what the **Build native** and **Build wasm** items in the Golo menu run; without them those two items report `command not found` and the rest of the menu is unaffected. | |
| 18 | + | |
| 19 | +## Install a release | |
| 20 | + | |
| 21 | +Precompiled binaries for macOS (Intel and Apple Silicon), Linux (amd64, arm64, 386) and Windows (amd64, arm64, 386) are on the [releases page](https://codeberg.org/TypeUnsafe/golo-script/releases). Each is one file: | |
| 22 | + | |
| 23 | +```bash | |
| 24 | +chmod +x golo-<version>-<platform> | |
| 25 | +sudo mv golo-<version>-<platform> /usr/local/bin/golo | |
| 26 | +golo --version | |
| 27 | +``` | |
| 28 | + | |
| 29 | +On macOS an unsigned download is quarantined until you say otherwise, in **System Settings ▸ Privacy & Security** or with `xattr -d com.apple.quarantine golo-<version>-<platform>`. | |
| 30 | + | |
| 31 | +Repeat for `gogolo` and `wagolo` if you want the compilers. | |
| 32 | + | |
| 33 | +## Or build from source | |
| 34 | + | |
| 35 | +This gives you all three at once, into `/usr/local/bin`. It needs Go; `wagolo` builds without TinyGo but cannot compile anything until TinyGo is installed. | |
| 36 | + | |
| 37 | +```bash | |
| 38 | +git clone https://codeberg.org/TypeUnsafe/golo-script.git && cd golo-script | |
| 39 | +./install.sh | |
| 40 | +``` | |
| 41 | + | |
| 42 | +Turbo Golo's own installer will do exactly that when asked: | |
| 43 | + | |
| 44 | +```bash | |
| 45 | +scripts/install.sh --with-server | |
| 46 | +``` | |
| 47 | + | |
| 48 | +It clones GoloScript into a temporary directory and runs its `install.sh`, which asks for `sudo` when it copies into `/usr/local/bin`. | |
| 49 | + | |
| 50 | +## Check it | |
| 51 | + | |
| 52 | +```bash | |
| 53 | +golo --version | |
| 54 | +``` | |
| 55 | + | |
| 56 | +``` | |
| 57 | +v0.1.1 | dev.20260802.🤓 | |
| 58 | +``` | |
| 59 | + | |
| 60 | +And the language server specifically — it is the interpreter, so this is a subcommand rather than a second binary: | |
| 61 | + | |
| 62 | +```bash | |
| 63 | +golo lsp </dev/null | |
| 64 | +``` | |
| 65 | + | |
| 66 | +It reads nothing, sees the end of its input, and exits cleanly. A `golo` that prints its usage or starts a REPL here is a different program with the same name. | |
| 67 | + | |
| 68 | +## Check that the editor finds it | |
| 69 | + | |
| 70 | +Open any `.golo` file and read the right-hand end of the status bar: | |
| 71 | + | |
| 72 | +``` | |
| 73 | + F1 Describe F2 Save F3 Open F6 Window F10 Menu 1:1 LSP: ready | |
| 74 | +``` | |
| 75 | + | |
| 76 | +`LSP: ready` means the server started. `LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases` means it was not found, and the message says where to get it. | |
| 77 | + | |
| 78 | +**Turbo Golo looks in two places, in order**: your `PATH`, then `/usr/local/bin`. The second is where GoloScript's installer writes, and it is searched even when it is not on `PATH` — a shell started by a desktop launcher, say — so that the case that otherwise looks like the server being broken works. | |
| 79 | + | |
| 80 | +## Variants | |
| 81 | + | |
| 82 | +- **You install binaries somewhere else.** Put that directory on `PATH`; the editor has no environment variable naming another place to look. A symbolic link in `/usr/local/bin` also works. | |
| 83 | +- **You already have `golo` but no completion.** Run `golo lsp </dev/null` and make sure it exits quietly. Then read the status bar — `Run ▸ Language server status` shows the path the editor found and whether the server answered its handshake. | |
| 84 | +- **You want to upgrade.** Replace the binary — a release download or `./install.sh` again from a fresh `git pull`. Restart the editor: it starts one server per session and does not notice a new binary until then. | |
| 85 | +- **You are installing for CI, or into an image.** GoloScript publishes a `scratch`-based image holding the interpreter alone: `docker run --rm -v "$PWD:/app" -w /app k33g/gololang:<tag> /golo ./main.golo`. The editor cannot use a server inside a container, so this is for running scripts, not for completion. | |
| 86 | +- **You want to be sure the editor is not simply finding it on `PATH`.** Start it with a stripped environment — `env PATH=/usr/bin:/bin turbo-golo main.golo` — and the status bar should still say `LSP: ready`, from `/usr/local/bin`. | |
| 87 | + | |
| 88 | +## What each binary is for, from the editor's side | |
| 89 | + | |
| 90 | +| Binary | What the editor uses it for | | |
| 91 | +| --- | --- | | |
| 92 | +| `golo` | `golo lsp` — completion, hover, definitions, the file's symbols and the error marks; and the **Run**, **Test**, **Test one**, **Debug**, **REPL** and **New script** items of the Golo menu | | |
| 93 | +| `gogolo` | The **Build native** item | | |
| 94 | +| `wagolo` | The **Build wasm** item | | |
| 95 | + | |
| 96 | +## See also | |
| 97 | + | |
| 98 | +- [How to enable completion](enable-completion.md) — what to do when the server is installed and still says nothing | |
| 99 | +- [How to run Golo commands from the editor](run-golo-commands.md) — the Golo menu | |
| 100 | +- [Colouring and completion](../explanation/colouring-and-completion.md) — why the interpreter is the server | |
| new file mode 100644 | |||
| @@ -0,0 +1,100 @@ | |||
| 1 | +# How to install GoloScript | ||
| 2 | + | ||
| 3 | +This guide shows how to get `golo` — and, if you want them, `gogolo` and `wagolo` — onto a machine, and how to check that Turbo Golo can find it. It assumes you already have Turbo Golo, or are about to — see [how to install the editor](install.md) for that. | ||
| 4 | + | ||
| 5 | +**The editor works without any of this.** Editing, colouring, themes, snippets and terminal windows all run with no interpreter at all. What needs it is completion, the error marks in the gutter, and every command in the Golo menu. | ||
| 6 | + | ||
| 7 | +## Which binary you need | ||
| 8 | + | ||
| 9 | +GoloScript ships three, under one version: | ||
| 10 | + | ||
| 11 | +| Binary | What it is for | Also needs | | ||
| 12 | +| --- | --- | --- | | ||
| 13 | +| `golo` | The interpreter: runs scripts, and is also the REPL, the debugger, the test runner and **the language server** | nothing | | ||
| 14 | +| `gogolo` | Compiles a script to a native executable, through Go | the Go toolchain | | ||
| 15 | +| `wagolo` | Compiles a script to WebAssembly, through Go and TinyGo | TinyGo, and `wasm-tools` for the `wasip2` target | | ||
| 16 | + | ||
| 17 | +The editor needs `golo` and nothing else. `gogolo` and `wagolo` are what the **Build native** and **Build wasm** items in the Golo menu run; without them those two items report `command not found` and the rest of the menu is unaffected. | ||
| 18 | + | ||
| 19 | +## Install a release | ||
| 20 | + | ||
| 21 | +Precompiled binaries for macOS (Intel and Apple Silicon), Linux (amd64, arm64, 386) and Windows (amd64, arm64, 386) are on the [releases page](https://codeberg.org/TypeUnsafe/golo-script/releases). Each is one file: | ||
| 22 | + | ||
| 23 | +```bash | ||
| 24 | +chmod +x golo-<version>-<platform> | ||
| 25 | +sudo mv golo-<version>-<platform> /usr/local/bin/golo | ||
| 26 | +golo --version | ||
| 27 | +``` | ||
| 28 | + | ||
| 29 | +On macOS an unsigned download is quarantined until you say otherwise, in **System Settings ▸ Privacy & Security** or with `xattr -d com.apple.quarantine golo-<version>-<platform>`. | ||
| 30 | + | ||
| 31 | +Repeat for `gogolo` and `wagolo` if you want the compilers. | ||
| 32 | + | ||
| 33 | +## Or build from source | ||
| 34 | + | ||
| 35 | +This gives you all three at once, into `/usr/local/bin`. It needs Go; `wagolo` builds without TinyGo but cannot compile anything until TinyGo is installed. | ||
| 36 | + | ||
| 37 | +```bash | ||
| 38 | +git clone https://codeberg.org/TypeUnsafe/golo-script.git && cd golo-script | ||
| 39 | +./install.sh | ||
| 40 | +``` | ||
| 41 | + | ||
| 42 | +Turbo Golo's own installer will do exactly that when asked: | ||
| 43 | + | ||
| 44 | +```bash | ||
| 45 | +scripts/install.sh --with-server | ||
| 46 | +``` | ||
| 47 | + | ||
| 48 | +It clones GoloScript into a temporary directory and runs its `install.sh`, which asks for `sudo` when it copies into `/usr/local/bin`. | ||
| 49 | + | ||
| 50 | +## Check it | ||
| 51 | + | ||
| 52 | +```bash | ||
| 53 | +golo --version | ||
| 54 | +``` | ||
| 55 | + | ||
| 56 | +``` | ||
| 57 | +v0.1.1 | dev.20260802.🤓 | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +And the language server specifically — it is the interpreter, so this is a subcommand rather than a second binary: | ||
| 61 | + | ||
| 62 | +```bash | ||
| 63 | +golo lsp </dev/null | ||
| 64 | +``` | ||
| 65 | + | ||
| 66 | +It reads nothing, sees the end of its input, and exits cleanly. A `golo` that prints its usage or starts a REPL here is a different program with the same name. | ||
| 67 | + | ||
| 68 | +## Check that the editor finds it | ||
| 69 | + | ||
| 70 | +Open any `.golo` file and read the right-hand end of the status bar: | ||
| 71 | + | ||
| 72 | +``` | ||
| 73 | + F1 Describe F2 Save F3 Open F6 Window F10 Menu 1:1 LSP: ready | ||
| 74 | +``` | ||
| 75 | + | ||
| 76 | +`LSP: ready` means the server started. `LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases` means it was not found, and the message says where to get it. | ||
| 77 | + | ||
| 78 | +**Turbo Golo looks in two places, in order**: your `PATH`, then `/usr/local/bin`. The second is where GoloScript's installer writes, and it is searched even when it is not on `PATH` — a shell started by a desktop launcher, say — so that the case that otherwise looks like the server being broken works. | ||
| 79 | + | ||
| 80 | +## Variants | ||
| 81 | + | ||
| 82 | +- **You install binaries somewhere else.** Put that directory on `PATH`; the editor has no environment variable naming another place to look. A symbolic link in `/usr/local/bin` also works. | ||
| 83 | +- **You already have `golo` but no completion.** Run `golo lsp </dev/null` and make sure it exits quietly. Then read the status bar — `Run ▸ Language server status` shows the path the editor found and whether the server answered its handshake. | ||
| 84 | +- **You want to upgrade.** Replace the binary — a release download or `./install.sh` again from a fresh `git pull`. Restart the editor: it starts one server per session and does not notice a new binary until then. | ||
| 85 | +- **You are installing for CI, or into an image.** GoloScript publishes a `scratch`-based image holding the interpreter alone: `docker run --rm -v "$PWD:/app" -w /app k33g/gololang:<tag> /golo ./main.golo`. The editor cannot use a server inside a container, so this is for running scripts, not for completion. | ||
| 86 | +- **You want to be sure the editor is not simply finding it on `PATH`.** Start it with a stripped environment — `env PATH=/usr/bin:/bin turbo-golo main.golo` — and the status bar should still say `LSP: ready`, from `/usr/local/bin`. | ||
| 87 | + | ||
| 88 | +## What each binary is for, from the editor's side | ||
| 89 | + | ||
| 90 | +| Binary | What the editor uses it for | | ||
| 91 | +| --- | --- | | ||
| 92 | +| `golo` | `golo lsp` — completion, hover, definitions, the file's symbols and the error marks; and the **Run**, **Test**, **Test one**, **Debug**, **REPL** and **New script** items of the Golo menu | | ||
| 93 | +| `gogolo` | The **Build native** item | | ||
| 94 | +| `wagolo` | The **Build wasm** item | | ||
| 95 | + | ||
| 96 | +## See also | ||
| 97 | + | ||
| 98 | +- [How to enable completion](enable-completion.md) — what to do when the server is installed and still says nothing | ||
| 99 | +- [How to run Golo commands from the editor](run-golo-commands.md) — the Golo menu | ||
| 100 | +- [Colouring and completion](../explanation/colouring-and-completion.md) — why the interpreter is the server | ||
added
docs/en/how-to/install.md +101 -0 | new file mode 100644 | ||
| @@ -0,0 +1,101 @@ | ||
| 1 | +# How to install and build Turbo Golo | |
| 2 | + | |
| 3 | +This guide shows how to get a working `turbo-golo` binary. It assumes you have Go 1.26 or later and can use a terminal. | |
| 4 | + | |
| 5 | +## The short way, from a checkout | |
| 6 | + | |
| 7 | +```bash | |
| 8 | +git clone ssh://git@rickub.com/turbo-editors/turbo-golo.git | |
| 9 | +cd turbo-golo | |
| 10 | +make install | |
| 11 | +``` | |
| 12 | + | |
| 13 | +That builds the editor, puts it where your shell looks for commands, and tells you what it found: the Go version it built with, where the binary went, whether that directory is on your `PATH`, and whether `golo` — the interpreter, which is also the language server — is installed. The build goes to a temporary file first, so a failed build never replaces a working installation. | |
| 14 | + | |
| 15 | +Then, from any directory holding Golo scripts: | |
| 16 | + | |
| 17 | +```bash | |
| 18 | +turbo-golo main.golo | |
| 19 | +``` | |
| 20 | + | |
| 21 | +### Options | |
| 22 | + | |
| 23 | +```bash | |
| 24 | +scripts/install.sh --prefix ~/bin # install somewhere of your choosing | |
| 25 | +scripts/install.sh --with-server # build and install GoloScript too | |
| 26 | +scripts/install.sh --uninstall # remove it again (make uninstall) | |
| 27 | +scripts/install.sh --help | |
| 28 | +``` | |
| 29 | + | |
| 30 | +Without `--prefix`, the editor goes where `go install` would put it: `$GOBIN`, or `$GOPATH/bin` when `GOBIN` is unset — usually `~/go/bin`. | |
| 31 | + | |
| 32 | +`--with-server` clones `https://codeberg.org/TypeUnsafe/golo-script` and runs its `install.sh`, which builds `golo`, `gogolo` and `wagolo` and puts them in `/usr/local/bin`. It needs `git` and Go; `wagolo` also needs TinyGo. Precompiled binaries are the other way — see [How to install GoloScript](install-goloscript.md). | |
| 33 | + | |
| 34 | +## Just build it, without installing | |
| 35 | + | |
| 36 | +```bash | |
| 37 | +make build | |
| 38 | +./bin/turbo-golo main.golo | |
| 39 | +``` | |
| 40 | + | |
| 41 | +`make build` also runs `scripts/check-version.sh` on what it built, so a binary that does not report the version the build meant fails the build rather than shipping. | |
| 42 | + | |
| 43 | +## From the module proxy, without a checkout | |
| 44 | + | |
| 45 | +```bash | |
| 46 | +go install rickub.com/turbo-editors/turbo-golo@latest | |
| 47 | +``` | |
| 48 | + | |
| 49 | +If the command is then "not found", the install directory is not on your `PATH`: | |
| 50 | + | |
| 51 | +```bash | |
| 52 | +export PATH="$PATH:$(go env GOPATH)/bin" | |
| 53 | +``` | |
| 54 | + | |
| 55 | +Turbo Golo has not been tagged yet, so until its first release `@latest` names the newest commit and the binary reports `devel` rather than a number; [the version number](../reference/versioning.md) explains why. | |
| 56 | + | |
| 57 | +## Check it works | |
| 58 | + | |
| 59 | +```bash | |
| 60 | +turbo-golo -version | |
| 61 | +turbo-golo -list-themes | |
| 62 | +``` | |
| 63 | + | |
| 64 | +The first names the commit the binary was built from, which is what to quote in a bug report; [the version number](../reference/versioning.md) explains what each form means. The second prints the themes compiled into the binary and tells you where your own would go. | |
| 65 | + | |
| 66 | +## Variants | |
| 67 | + | |
| 68 | +- **You only want to run it once**: `go run rickub.com/turbo-editors/turbo-golo@latest demos/hello/hello.golo` | |
| 69 | +- **You want the binary somewhere specific**: `go build -o /usr/local/bin/turbo-golo .` | |
| 70 | +- **Your terminal has no true colour**: use `turbo-golo -theme turbo-classic`, which is built from the sixteen ANSI colours only. `turbo-dark` and `borland-light` use 24-bit colours. | |
| 71 | + | |
| 72 | +## When something goes wrong | |
| 73 | + | |
| 74 | +**`the installed binary does not run`.** The installer prints whatever the system said just above that line — read it first, because it names the actual problem. | |
| 75 | + | |
| 76 | +The installer replaces the binary rather than writing over the one that is there, so a reinstall gives the file a fresh identity. That matters on macOS, which caches a binary's code signature against its inode: writing new bytes into the old inode leaves the cached signature describing something else, and the kernel then refuses to run a binary that built and installed perfectly. If you have an older copy installed by something that used `cp`, removing it first clears any such state: | |
| 77 | + | |
| 78 | +```bash | |
| 79 | +scripts/install.sh --uninstall | |
| 80 | +scripts/install.sh | |
| 81 | +``` | |
| 82 | + | |
| 83 | +**`Go x.y or later is needed`.** The editor is written in Go, so building it needs a Go toolchain even though it is an editor for Golo. The version comes from `go.mod`, so it cannot drift from what the code actually needs. | |
| 84 | + | |
| 85 | +**`build failed; nothing was installed`.** Your existing installation is untouched — the build goes to a temporary file first. The compiler's own output is printed above the message. | |
| 86 | + | |
| 87 | +**`the build did not carry its version; nothing was installed`.** The binary built but does not report the version the installer stamped into it — a linker flag naming a symbol that no longer exists, usually. Nothing is installed; the check that failed is described under [the version number](../reference/versioning.md#checked-at-build-time). | |
| 88 | + | |
| 89 | +**`golo is not installed, so there will be no completion`.** Not an error: editing, colouring and themes all work without it. Install GoloScript when you want completion — [How to install GoloScript](install-goloscript.md) — or re-run the installer with `--with-server`. | |
| 90 | + | |
| 91 | +## Terminal requirements | |
| 92 | + | |
| 93 | +Turbo Golo needs a terminal that reports its size and supports mouse reporting — every mainstream one does. It reads `TERM` through tcell; if the display is wrong, check that `TERM` matches your terminal (`xterm-256color` is a safe default). | |
| 94 | + | |
| 95 | +## See also | |
| 96 | + | |
| 97 | +- Every flag: [command line reference](../reference/cli.md) | |
| 98 | +- Getting completion working: [How to enable Golo completion](enable-completion.md) | |
| 99 | +- The interpreter itself: [How to install GoloScript](install-goloscript.md) | |
| 100 | +- A guided first session: [Your first Golo program in Turbo Golo](../tutorials/getting-started.md) | |
| 101 | +- Programs to try it on: [the demos](../../../demos/) | |
| new file mode 100644 | |||
| @@ -0,0 +1,101 @@ | |||
| 1 | +# How to install and build Turbo Golo | ||
| 2 | + | ||
| 3 | +This guide shows how to get a working `turbo-golo` binary. It assumes you have Go 1.26 or later and can use a terminal. | ||
| 4 | + | ||
| 5 | +## The short way, from a checkout | ||
| 6 | + | ||
| 7 | +```bash | ||
| 8 | +git clone ssh://git@rickub.com/turbo-editors/turbo-golo.git | ||
| 9 | +cd turbo-golo | ||
| 10 | +make install | ||
| 11 | +``` | ||
| 12 | + | ||
| 13 | +That builds the editor, puts it where your shell looks for commands, and tells you what it found: the Go version it built with, where the binary went, whether that directory is on your `PATH`, and whether `golo` — the interpreter, which is also the language server — is installed. The build goes to a temporary file first, so a failed build never replaces a working installation. | ||
| 14 | + | ||
| 15 | +Then, from any directory holding Golo scripts: | ||
| 16 | + | ||
| 17 | +```bash | ||
| 18 | +turbo-golo main.golo | ||
| 19 | +``` | ||
| 20 | + | ||
| 21 | +### Options | ||
| 22 | + | ||
| 23 | +```bash | ||
| 24 | +scripts/install.sh --prefix ~/bin # install somewhere of your choosing | ||
| 25 | +scripts/install.sh --with-server # build and install GoloScript too | ||
| 26 | +scripts/install.sh --uninstall # remove it again (make uninstall) | ||
| 27 | +scripts/install.sh --help | ||
| 28 | +``` | ||
| 29 | + | ||
| 30 | +Without `--prefix`, the editor goes where `go install` would put it: `$GOBIN`, or `$GOPATH/bin` when `GOBIN` is unset — usually `~/go/bin`. | ||
| 31 | + | ||
| 32 | +`--with-server` clones `https://codeberg.org/TypeUnsafe/golo-script` and runs its `install.sh`, which builds `golo`, `gogolo` and `wagolo` and puts them in `/usr/local/bin`. It needs `git` and Go; `wagolo` also needs TinyGo. Precompiled binaries are the other way — see [How to install GoloScript](install-goloscript.md). | ||
| 33 | + | ||
| 34 | +## Just build it, without installing | ||
| 35 | + | ||
| 36 | +```bash | ||
| 37 | +make build | ||
| 38 | +./bin/turbo-golo main.golo | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +`make build` also runs `scripts/check-version.sh` on what it built, so a binary that does not report the version the build meant fails the build rather than shipping. | ||
| 42 | + | ||
| 43 | +## From the module proxy, without a checkout | ||
| 44 | + | ||
| 45 | +```bash | ||
| 46 | +go install rickub.com/turbo-editors/turbo-golo@latest | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +If the command is then "not found", the install directory is not on your `PATH`: | ||
| 50 | + | ||
| 51 | +```bash | ||
| 52 | +export PATH="$PATH:$(go env GOPATH)/bin" | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +Turbo Golo has not been tagged yet, so until its first release `@latest` names the newest commit and the binary reports `devel` rather than a number; [the version number](../reference/versioning.md) explains why. | ||
| 56 | + | ||
| 57 | +## Check it works | ||
| 58 | + | ||
| 59 | +```bash | ||
| 60 | +turbo-golo -version | ||
| 61 | +turbo-golo -list-themes | ||
| 62 | +``` | ||
| 63 | + | ||
| 64 | +The first names the commit the binary was built from, which is what to quote in a bug report; [the version number](../reference/versioning.md) explains what each form means. The second prints the themes compiled into the binary and tells you where your own would go. | ||
| 65 | + | ||
| 66 | +## Variants | ||
| 67 | + | ||
| 68 | +- **You only want to run it once**: `go run rickub.com/turbo-editors/turbo-golo@latest demos/hello/hello.golo` | ||
| 69 | +- **You want the binary somewhere specific**: `go build -o /usr/local/bin/turbo-golo .` | ||
| 70 | +- **Your terminal has no true colour**: use `turbo-golo -theme turbo-classic`, which is built from the sixteen ANSI colours only. `turbo-dark` and `borland-light` use 24-bit colours. | ||
| 71 | + | ||
| 72 | +## When something goes wrong | ||
| 73 | + | ||
| 74 | +**`the installed binary does not run`.** The installer prints whatever the system said just above that line — read it first, because it names the actual problem. | ||
| 75 | + | ||
| 76 | +The installer replaces the binary rather than writing over the one that is there, so a reinstall gives the file a fresh identity. That matters on macOS, which caches a binary's code signature against its inode: writing new bytes into the old inode leaves the cached signature describing something else, and the kernel then refuses to run a binary that built and installed perfectly. If you have an older copy installed by something that used `cp`, removing it first clears any such state: | ||
| 77 | + | ||
| 78 | +```bash | ||
| 79 | +scripts/install.sh --uninstall | ||
| 80 | +scripts/install.sh | ||
| 81 | +``` | ||
| 82 | + | ||
| 83 | +**`Go x.y or later is needed`.** The editor is written in Go, so building it needs a Go toolchain even though it is an editor for Golo. The version comes from `go.mod`, so it cannot drift from what the code actually needs. | ||
| 84 | + | ||
| 85 | +**`build failed; nothing was installed`.** Your existing installation is untouched — the build goes to a temporary file first. The compiler's own output is printed above the message. | ||
| 86 | + | ||
| 87 | +**`the build did not carry its version; nothing was installed`.** The binary built but does not report the version the installer stamped into it — a linker flag naming a symbol that no longer exists, usually. Nothing is installed; the check that failed is described under [the version number](../reference/versioning.md#checked-at-build-time). | ||
| 88 | + | ||
| 89 | +**`golo is not installed, so there will be no completion`.** Not an error: editing, colouring and themes all work without it. Install GoloScript when you want completion — [How to install GoloScript](install-goloscript.md) — or re-run the installer with `--with-server`. | ||
| 90 | + | ||
| 91 | +## Terminal requirements | ||
| 92 | + | ||
| 93 | +Turbo Golo needs a terminal that reports its size and supports mouse reporting — every mainstream one does. It reads `TERM` through tcell; if the display is wrong, check that `TERM` matches your terminal (`xterm-256color` is a safe default). | ||
| 94 | + | ||
| 95 | +## See also | ||
| 96 | + | ||
| 97 | +- Every flag: [command line reference](../reference/cli.md) | ||
| 98 | +- Getting completion working: [How to enable Golo completion](enable-completion.md) | ||
| 99 | +- The interpreter itself: [How to install GoloScript](install-goloscript.md) | ||
| 100 | +- A guided first session: [Your first Golo program in Turbo Golo](../tutorials/getting-started.md) | ||
| 101 | +- Programs to try it on: [the demos](../../../demos/) | ||
added
docs/en/how-to/make-a-release.md +105 -0 | new file mode 100644 | ||
| @@ -0,0 +1,105 @@ | ||
| 1 | +# How to make a release | |
| 2 | + | |
| 3 | +This guide shows how to cut a release so that the editor reports its own version correctly. It assumes you can push to the repository. Turbo Golo has not been released yet, so the first time through, the tag you create is `v0.1.0` — the one `release.env` already names. | |
| 4 | + | |
| 5 | +## Check what you are about to release | |
| 6 | + | |
| 7 | +```sh | |
| 8 | +make version | |
| 9 | +``` | |
| 10 | + | |
| 11 | +``` | |
| 12 | +v0.1.0-14-g88a4c38 (88a4c38) | |
| 13 | +``` | |
| 14 | + | |
| 15 | +Fourteen commits past `v0.1.0`. A `-dirty` on the end means you have uncommitted changes — commit or stash them first, or the release will carry that suffix for ever. | |
| 16 | + | |
| 17 | +In a checkout with no tag at all — which is where Turbo Golo starts — `git describe` has nothing to describe, and the line reads `devel (88a4c38)` instead. That is expected before the first tag, and it is why the release scripts stamp the tag themselves rather than trusting `git describe`. | |
| 18 | + | |
| 19 | +## Tag it | |
| 20 | + | |
| 21 | +```sh | |
| 22 | +git tag -a v0.1.0 -m "v0.1.0" | |
| 23 | +git push origin v0.1.0 | |
| 24 | +``` | |
| 25 | + | |
| 26 | +The tag is what the version comes from, so it has to exist before you build anything you intend to hand out. Annotated (`-a`) rather than lightweight, because `git describe` prefers annotated tags. | |
| 27 | + | |
| 28 | +## Build the release binary | |
| 29 | + | |
| 30 | +```sh | |
| 31 | +make build | |
| 32 | +./bin/turbo-golo -version | |
| 33 | +``` | |
| 34 | + | |
| 35 | +``` | |
| 36 | +Turbo Golo 0.1.0 (88a4c38, built 2026-09-14T18:04:05Z) | |
| 37 | +``` | |
| 38 | + | |
| 39 | +No `-14-g…` suffix: you are exactly on the tag. That is what tells you the tag took. | |
| 40 | + | |
| 41 | +## Check the About box | |
| 42 | + | |
| 43 | +Start the editor and press `Alt-H`, then `A`. | |
| 44 | + | |
| 45 | +``` | |
| 46 | +Turbo Golo 0.1.0 | |
| 47 | + | |
| 48 | +A Turbo C-style editor for Golo, | |
| 49 | +written in Go. | |
| 50 | + | |
| 51 | +Commit: 88a4c38 | |
| 52 | +Built: 2026-09-14 18:04 UTC | |
| 53 | +Theme: Turbo Classic | |
| 54 | +``` | |
| 55 | + | |
| 56 | +## Or use the scripts and let the workflow publish | |
| 57 | + | |
| 58 | +That is the way a release is actually cut. Put the version and its one-line description in `release.env` — it is gitignored, so CI never sees it: | |
| 59 | + | |
| 60 | +```sh | |
| 61 | +TAG="v1.0.0" | |
| 62 | +ABOUT="Turbo Golo" | |
| 63 | +``` | |
| 64 | + | |
| 65 | +Then run one script: | |
| 66 | + | |
| 67 | +```sh | |
| 68 | +./01-release.tag.sh | |
| 69 | +``` | |
| 70 | + | |
| 71 | +It runs `make check`, refuses a tag already taken locally or on `origin`, refuses a `go.mod` carrying a `replace` directive, commits anything outstanding, pushes the branch, and only then tags and pushes the tag. That order matters: a tag pushed before the branch points at a commit the remote has never seen, and a tag created before a failed push is left behind for somebody to find. | |
| 72 | + | |
| 73 | +That is the last thing you run by hand. Pushing the tag starts `.github/workflows/release.yml`; follow it on the repository's Actions tab. It runs the suite, cross-compiles the binaries with `./02-build-releases.sh` — the same script you can run on your machine — and creates the release page with them: the tag's message, the `go install` line, links to the documentation **at that tag**, one binary per platform, the `SHA256SUMS` and the README that describes the downloads. | |
| 74 | + | |
| 75 | +The job publishes with its own `GITHUB_TOKEN`, which is the only credential Rickub's release API accepts — a personal token is refused. There is nothing to configure and no secret to keep, which is why the old `02-release.publish.sh` and `04-release.upload-binaries.sh` are gone. | |
| 76 | + | |
| 77 | +`02-build-releases.sh` cross-compiles every platform and **stamps `TAG` itself**, by overriding the Makefile's version: `make ldflags VERSION=v1.0.0`. The release *is* `v1.0.0`, so that is what its binaries say — whatever `git describe` would have answered, and whether or not the tag has been created yet. It then runs the staged binary for this machine and checks it reports the version, which is the only proof that what ships carries it. | |
| 78 | + | |
| 79 | +You can see what the workflow will publish without publishing anything, or build the binaries by hand: | |
| 80 | + | |
| 81 | +```sh | |
| 82 | +./02-build-releases.sh v1.0.0 # writes release/v1.0.0/, pushes nothing | |
| 83 | +``` | |
| 84 | + | |
| 85 | +With no argument it reads `TAG` from `release.env`; the workflow has no `release.env`, so it passes the tag it was started by. | |
| 86 | + | |
| 87 | +The suite includes tests that run `01-release.tag.sh` against a throwaway clone. They skip themselves when `TURBO_GOLO_RELEASING` is set, which the script exports before calling `make check` — removing that line makes a release recurse until something runs out. The workflow sets the same variable for its own `go test` step. | |
| 88 | + | |
| 89 | +## Variants | |
| 90 | + | |
| 91 | +- **You install rather than distribute a binary.** `make install` and `scripts/install.sh` stamp the same way, so an installed editor names the commit it came from. There is nothing extra to do. | |
| 92 | +- **Someone installs with `go install`.** `go install rickub.com/turbo-editors/turbo-golo@v0.1.0` reports `0.1.0` from the module version, with no commit and no build date. That is the Go tool's own record; nothing needs stamping. | |
| 93 | +- **You tagged the wrong commit.** If the tag has not been pushed, delete it (`git tag -d v1.0.0`), tag the right one, and rebuild. Once it is on `origin`, do not move it: the module proxy has cached `go install …@v1.0.0` and the release page already carries binaries with that number, which is why `01-release.tag.sh` refuses a tag that exists. Bump `TAG` and release again. | |
| 94 | +- **About says `devel`.** The binary was built with a plain `go build .` rather than through `make`, or from a checkout with no tag yet. Nothing is wrong with it; it simply has no tag stamped, because the Go build system does not read git tags. Use `make build` on a tagged commit. | |
| 95 | +- **About says `unknown`.** Nothing named the build at all — a `go run .`, or a build from a directory with no git history. Use `make build` from the checkout. | |
| 96 | +- **You have no git at all**, having downloaded a source archive. `make build` still works and the binary reports `unknown`. Pass the version yourself if you need one: | |
| 97 | + ```sh | |
| 98 | + go build -ldflags "-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.1.0'" -o bin/turbo-golo . | |
| 99 | + ``` | |
| 100 | + | |
| 101 | +## See also | |
| 102 | + | |
| 103 | +- Every source of the number, and what each build reports: [The version number](../reference/versioning.md) | |
| 104 | +- Why there is no version constant in the source: [Design decisions](../explanation/design-decisions.md#the-version-is-a-property-of-the-build-not-of-the-source) | |
| 105 | +- Installing onto your PATH: [How to install and build Turbo Golo](install.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,105 @@ | |||
| 1 | +# How to make a release | ||
| 2 | + | ||
| 3 | +This guide shows how to cut a release so that the editor reports its own version correctly. It assumes you can push to the repository. Turbo Golo has not been released yet, so the first time through, the tag you create is `v0.1.0` — the one `release.env` already names. | ||
| 4 | + | ||
| 5 | +## Check what you are about to release | ||
| 6 | + | ||
| 7 | +```sh | ||
| 8 | +make version | ||
| 9 | +``` | ||
| 10 | + | ||
| 11 | +``` | ||
| 12 | +v0.1.0-14-g88a4c38 (88a4c38) | ||
| 13 | +``` | ||
| 14 | + | ||
| 15 | +Fourteen commits past `v0.1.0`. A `-dirty` on the end means you have uncommitted changes — commit or stash them first, or the release will carry that suffix for ever. | ||
| 16 | + | ||
| 17 | +In a checkout with no tag at all — which is where Turbo Golo starts — `git describe` has nothing to describe, and the line reads `devel (88a4c38)` instead. That is expected before the first tag, and it is why the release scripts stamp the tag themselves rather than trusting `git describe`. | ||
| 18 | + | ||
| 19 | +## Tag it | ||
| 20 | + | ||
| 21 | +```sh | ||
| 22 | +git tag -a v0.1.0 -m "v0.1.0" | ||
| 23 | +git push origin v0.1.0 | ||
| 24 | +``` | ||
| 25 | + | ||
| 26 | +The tag is what the version comes from, so it has to exist before you build anything you intend to hand out. Annotated (`-a`) rather than lightweight, because `git describe` prefers annotated tags. | ||
| 27 | + | ||
| 28 | +## Build the release binary | ||
| 29 | + | ||
| 30 | +```sh | ||
| 31 | +make build | ||
| 32 | +./bin/turbo-golo -version | ||
| 33 | +``` | ||
| 34 | + | ||
| 35 | +``` | ||
| 36 | +Turbo Golo 0.1.0 (88a4c38, built 2026-09-14T18:04:05Z) | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +No `-14-g…` suffix: you are exactly on the tag. That is what tells you the tag took. | ||
| 40 | + | ||
| 41 | +## Check the About box | ||
| 42 | + | ||
| 43 | +Start the editor and press `Alt-H`, then `A`. | ||
| 44 | + | ||
| 45 | +``` | ||
| 46 | +Turbo Golo 0.1.0 | ||
| 47 | + | ||
| 48 | +A Turbo C-style editor for Golo, | ||
| 49 | +written in Go. | ||
| 50 | + | ||
| 51 | +Commit: 88a4c38 | ||
| 52 | +Built: 2026-09-14 18:04 UTC | ||
| 53 | +Theme: Turbo Classic | ||
| 54 | +``` | ||
| 55 | + | ||
| 56 | +## Or use the scripts and let the workflow publish | ||
| 57 | + | ||
| 58 | +That is the way a release is actually cut. Put the version and its one-line description in `release.env` — it is gitignored, so CI never sees it: | ||
| 59 | + | ||
| 60 | +```sh | ||
| 61 | +TAG="v1.0.0" | ||
| 62 | +ABOUT="Turbo Golo" | ||
| 63 | +``` | ||
| 64 | + | ||
| 65 | +Then run one script: | ||
| 66 | + | ||
| 67 | +```sh | ||
| 68 | +./01-release.tag.sh | ||
| 69 | +``` | ||
| 70 | + | ||
| 71 | +It runs `make check`, refuses a tag already taken locally or on `origin`, refuses a `go.mod` carrying a `replace` directive, commits anything outstanding, pushes the branch, and only then tags and pushes the tag. That order matters: a tag pushed before the branch points at a commit the remote has never seen, and a tag created before a failed push is left behind for somebody to find. | ||
| 72 | + | ||
| 73 | +That is the last thing you run by hand. Pushing the tag starts `.github/workflows/release.yml`; follow it on the repository's Actions tab. It runs the suite, cross-compiles the binaries with `./02-build-releases.sh` — the same script you can run on your machine — and creates the release page with them: the tag's message, the `go install` line, links to the documentation **at that tag**, one binary per platform, the `SHA256SUMS` and the README that describes the downloads. | ||
| 74 | + | ||
| 75 | +The job publishes with its own `GITHUB_TOKEN`, which is the only credential Rickub's release API accepts — a personal token is refused. There is nothing to configure and no secret to keep, which is why the old `02-release.publish.sh` and `04-release.upload-binaries.sh` are gone. | ||
| 76 | + | ||
| 77 | +`02-build-releases.sh` cross-compiles every platform and **stamps `TAG` itself**, by overriding the Makefile's version: `make ldflags VERSION=v1.0.0`. The release *is* `v1.0.0`, so that is what its binaries say — whatever `git describe` would have answered, and whether or not the tag has been created yet. It then runs the staged binary for this machine and checks it reports the version, which is the only proof that what ships carries it. | ||
| 78 | + | ||
| 79 | +You can see what the workflow will publish without publishing anything, or build the binaries by hand: | ||
| 80 | + | ||
| 81 | +```sh | ||
| 82 | +./02-build-releases.sh v1.0.0 # writes release/v1.0.0/, pushes nothing | ||
| 83 | +``` | ||
| 84 | + | ||
| 85 | +With no argument it reads `TAG` from `release.env`; the workflow has no `release.env`, so it passes the tag it was started by. | ||
| 86 | + | ||
| 87 | +The suite includes tests that run `01-release.tag.sh` against a throwaway clone. They skip themselves when `TURBO_GOLO_RELEASING` is set, which the script exports before calling `make check` — removing that line makes a release recurse until something runs out. The workflow sets the same variable for its own `go test` step. | ||
| 88 | + | ||
| 89 | +## Variants | ||
| 90 | + | ||
| 91 | +- **You install rather than distribute a binary.** `make install` and `scripts/install.sh` stamp the same way, so an installed editor names the commit it came from. There is nothing extra to do. | ||
| 92 | +- **Someone installs with `go install`.** `go install rickub.com/turbo-editors/turbo-golo@v0.1.0` reports `0.1.0` from the module version, with no commit and no build date. That is the Go tool's own record; nothing needs stamping. | ||
| 93 | +- **You tagged the wrong commit.** If the tag has not been pushed, delete it (`git tag -d v1.0.0`), tag the right one, and rebuild. Once it is on `origin`, do not move it: the module proxy has cached `go install …@v1.0.0` and the release page already carries binaries with that number, which is why `01-release.tag.sh` refuses a tag that exists. Bump `TAG` and release again. | ||
| 94 | +- **About says `devel`.** The binary was built with a plain `go build .` rather than through `make`, or from a checkout with no tag yet. Nothing is wrong with it; it simply has no tag stamped, because the Go build system does not read git tags. Use `make build` on a tagged commit. | ||
| 95 | +- **About says `unknown`.** Nothing named the build at all — a `go run .`, or a build from a directory with no git history. Use `make build` from the checkout. | ||
| 96 | +- **You have no git at all**, having downloaded a source archive. `make build` still works and the binary reports `unknown`. Pass the version yourself if you need one: | ||
| 97 | + ```sh | ||
| 98 | + go build -ldflags "-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.1.0'" -o bin/turbo-golo . | ||
| 99 | + ``` | ||
| 100 | + | ||
| 101 | +## See also | ||
| 102 | + | ||
| 103 | +- Every source of the number, and what each build reports: [The version number](../reference/versioning.md) | ||
| 104 | +- Why there is no version constant in the source: [Design decisions](../explanation/design-decisions.md#the-version-is-a-property-of-the-build-not-of-the-source) | ||
| 105 | +- Installing onto your PATH: [How to install and build Turbo Golo](install.md) | ||
added
docs/en/how-to/run-golo-commands.md +230 -0 | new file mode 100644 | ||
| @@ -0,0 +1,230 @@ | ||
| 1 | +# How to run Golo commands from the editor | |
| 2 | + | |
| 3 | +This guide shows how to run, test, debug and compile your scripts without leaving Turbo Golo. It assumes the editor is installed and you have a directory with a `.golo` file in it. | |
| 4 | + | |
| 5 | +## Get a starter file | |
| 6 | + | |
| 7 | +Start the editor **from the directory your scripts live in**, then choose **Golo ▸ Create tools file** (`Alt-G`, then `C`). | |
| 8 | + | |
| 9 | +That writes `.turbo-golo/tools.toml` with the commands a Golo programmer runs most, and opens it. The first three: | |
| 10 | + | |
| 11 | +```toml | |
| 12 | +[[tool]] | |
| 13 | +name = "~R~un" | |
| 14 | +# The interpreter, on the file you name. A terminal, not a popup: a script that | |
| 15 | +# reads the keyboard has to be able to be answered, and one that runs long has | |
| 16 | +# to be able to be interrupted. | |
| 17 | +command = "golo {{script, e.g. main.golo}}" | |
| 18 | +output = "terminal" | |
| 19 | + | |
| 20 | +[[tool]] | |
| 21 | +name = "~T~est" | |
| 22 | +# Every *_test.golo under the current directory, with gololang.Testing. | |
| 23 | +command = "golo --test" | |
| 24 | +output = "popup" | |
| 25 | + | |
| 26 | +[[tool]] | |
| 27 | +name = "Test ~o~ne" | |
| 28 | +command = "golo --test {{test file or directory}}" | |
| 29 | +output = "popup" | |
| 30 | +``` | |
| 31 | + | |
| 32 | +Each `[[tool]]` becomes one line of the **Golo** menu, in the order they appear — unless it names a `menu` of its own, which a later section covers. The file is read every time the menu opens, so an edit takes effect immediately. | |
| 33 | + | |
| 34 | +## Run one | |
| 35 | + | |
| 36 | +`Alt-G`, then the letter between the tildes — `R` to run, `T` to test. | |
| 37 | + | |
| 38 | +**Run** asks first, because Golo has no manifest that says which file is the program: | |
| 39 | + | |
| 40 | +``` | |
| 41 | +┌──────────────── Run ────────────────┐ | |
| 42 | +│ script, e.g. main.golo │ | |
| 43 | +│ [ ] │ | |
| 44 | +└─────────────────────────────────────┘ | |
| 45 | +``` | |
| 46 | + | |
| 47 | +Type `main.golo` and press `Enter`. A terminal window opens and the script runs in it; when it ends the window stays, showing what it printed. Press `Ctrl-W` to close it. Run it again and the box remembers the name for the rest of the session. | |
| 48 | + | |
| 49 | +**Test** opens a **popup** at once, which fills in as the command runs. Its title carries the command and, once it has ended, how it went: | |
| 50 | + | |
| 51 | +``` | |
| 52 | +┌──────────────── golo --test — ok ─────────────────┐ | |
| 53 | +│ 🧪 Running Golo tests... │ | |
| 54 | +│ │ | |
| 55 | +│ 📝 shapes_test.golo │ | |
| 56 | +│ ✓ a point describes itself │ | |
| 57 | +│ ✅ 1 test(s) passed │ | |
| 58 | +│ │ | |
| 59 | +│ [ Close ] │ | |
| 60 | +└────────────────────────────────────────────────────┘ | |
| 61 | +``` | |
| 62 | + | |
| 63 | +| Key | Effect | | |
| 64 | +| --- | --- | | |
| 65 | +| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Read through the output | | |
| 66 | +| `Escape` | Close it — and **stop the command** if it is still running | | |
| 67 | +| `Enter` | Close it | | |
| 68 | + | |
| 69 | +A command that succeeded silently shows `(no output)` rather than a blank box, so you can tell it from one that has not started. | |
| 70 | + | |
| 71 | +## The rest of the menu | |
| 72 | + | |
| 73 | +| Item | What it runs | Where | | |
| 74 | +| --- | --- | --- | | |
| 75 | +| **Debug** | `golo --debug <script>` — the interpreter with its step debugger on | a terminal, because the debugger reads the keyboard | | |
| 76 | +| **REPL** | `golo` — the read-eval-print loop | a terminal | | |
| 77 | +| **New script** | `golo new main --module <name> --name <file>` — a starter program from GoloScript's own template | a popup; the new file appears in the project tree | | |
| 78 | +| **Build native** | `gogolo build -o <out> <script>` — Golo → Go → a native executable | a popup; needs the Go toolchain | | |
| 79 | +| **Build wasm** | `wagolo build -target=<wasi\|js\|wasip2> -o <out.wasm> <script>` — Golo → Go → TinyGo → WebAssembly | a popup; needs TinyGo, and `wasm-tools` for `wasip2` | | |
| 80 | + | |
| 81 | +`Build native` can take a while: it runs the Go compiler. The popup is modal, so while it runs you cannot type anywhere else; `Escape` closes it and stops the compile. | |
| 82 | + | |
| 83 | +## Choose where the output goes | |
| 84 | + | |
| 85 | +Set `output` on a tool: | |
| 86 | + | |
| 87 | +| `output` | What you get | | |
| 88 | +| --- | --- | | |
| 89 | +| `popup` | A dialog that fills in as it runs. The default. | | |
| 90 | +| `terminal` | A terminal window: colours, `Ctrl-C`, and the keyboard reaches the program | | |
| 91 | +| `editor` | An editing window once it has finished, to search with `Ctrl-F` | | |
| 92 | + | |
| 93 | +`Run`, `Debug` and `REPL` are `terminal` in the starter file, and they are the example of why the key exists: a popup cannot answer a script that calls `readln`, cannot be interrupted with `Ctrl-C` while `httpServe` is listening, and cannot be a REPL at all. | |
| 94 | + | |
| 95 | +Reach for `editor` when the output is something to work through — the Go source that `gogolo transpile main.golo` prints, or a long test report you want to search. | |
| 96 | + | |
| 97 | +## A long command holds the editor | |
| 98 | + | |
| 99 | +A popup is modal: while `gogolo build` runs, you cannot type anywhere else. `Escape` closes it and stops the command. | |
| 100 | + | |
| 101 | +If that gets in the way for a particular command, give it `output = "terminal"` — the window is an ordinary one and you can carry on working beside it. That is what making the key configurable is for. | |
| 102 | + | |
| 103 | +## What happens to your open files | |
| 104 | + | |
| 105 | +Golo has no formatter, so nothing in the starter file rewrites the file you are looking at. But `New script` writes a new file into the directory, `gogolo build -keep-go` leaves a `.go` beside your script, and a tool of your own may do anything. When a command finishes, the editor **re-reads every open file that has no unsaved changes**, so a file another command changed appears as it now is, and the project tree is refreshed so a new one shows up. The status bar says how many. | |
| 106 | + | |
| 107 | +A file with unsaved changes is **left alone**, and the status bar says so too: | |
| 108 | + | |
| 109 | +``` | |
| 110 | +Reloaded 2 files; 1 file with unsaved changes left alone | |
| 111 | +``` | |
| 112 | + | |
| 113 | +That is deliberate: your edit and the command genuinely disagree, and the editor is not the one that should decide which wins. Save first (`F2`) and run the command again, or keep editing. | |
| 114 | + | |
| 115 | +## Add your own commands | |
| 116 | + | |
| 117 | +Edit `.turbo-golo/tools.toml`. A command goes to `sh -c`, so one entry can be a whole sequence: | |
| 118 | + | |
| 119 | +```toml | |
| 120 | +[[tool]] | |
| 121 | +name = "Test and ~b~uild" | |
| 122 | +command = "golo --test && gogolo build -o bin/app main.golo" | |
| 123 | +output = "popup" | |
| 124 | + | |
| 125 | +[[tool]] | |
| 126 | +name = "~T~ranspile" | |
| 127 | +command = "gogolo transpile {{script, e.g. main.golo}}" | |
| 128 | +output = "editor" | |
| 129 | + | |
| 130 | +[[tool]] | |
| 131 | +name = "Run in ~D~ocker" | |
| 132 | +command = "docker run --rm -v \"$PWD:/app\" -w /app k33g/gololang:latest /golo ./{{script}}" | |
| 133 | +output = "terminal" | |
| 134 | +``` | |
| 135 | + | |
| 136 | +Give each a hot key with tildes, and keep them distinct — the menu answers the first match it finds. | |
| 137 | + | |
| 138 | +## Put a tool in a menu of its own | |
| 139 | + | |
| 140 | +A tool that has nothing to do with Golo does not belong in the Golo menu. Give it a `menu`: | |
| 141 | + | |
| 142 | +```toml | |
| 143 | +[[tool]] | |
| 144 | +name = "~E~cho" | |
| 145 | +command = "echo TADA" | |
| 146 | +output = "terminal" | |
| 147 | +menu = "Tools" | |
| 148 | + | |
| 149 | +[[tool]] | |
| 150 | +name = "~U~p" | |
| 151 | +command = "docker compose up -d" | |
| 152 | +menu = "Docker" | |
| 153 | + | |
| 154 | +[[tool]] | |
| 155 | +name = "~D~own" | |
| 156 | +command = "docker compose down" | |
| 157 | +menu = "Docker" | |
| 158 | +``` | |
| 159 | + | |
| 160 | +That gives you a **Tools** menu and a **Docker** menu on the bar, between Golo and Help, in the order the names first appear in the file. Docker holds both its tools. Nothing needs restarting: save the file and the bar follows. | |
| 161 | + | |
| 162 | +The name is yours to choose — there is no list to pick from. Leave `menu` out and the tool stays in Golo, which is where eight of the nine starter commands are. | |
| 163 | + | |
| 164 | +### The hot key is chosen for you | |
| 165 | + | |
| 166 | +You cannot know, when writing the file, which letters the editor's own menus have taken. So it works it out: the first letter of the name that nothing else claims gets the tildes. | |
| 167 | + | |
| 168 | +`Tools` gets `Alt-T`, because `T` is free. A menu called `Format` would get `Alt-A`, because `F` is File's, `o` is Options' and `r` is Run's. A menu called `Go` would get `Alt-O`… no — `O` is Options'; it would get no hot key at all, because `G` is Golo's, and `F10` would be the way to it. | |
| 169 | + | |
| 170 | +Write the tildes yourself — `menu = "Doc~k~er"` — and a free letter is kept. A taken one is not: the bar answers the *first* menu matching a key, so honouring your choice would make one of the two menus unreachable. It picks another letter and says nothing. | |
| 171 | + | |
| 172 | +## Variants | |
| 173 | + | |
| 174 | +- **You have one script and never another.** Replace `golo {{script, e.g. main.golo}}` with `golo main.golo`, and the box stops appearing. The placeholder is there because a starter file cannot know which file is the program. | |
| 175 | +- **You started the editor from a subdirectory.** Commands run there, and relative paths in the box are relative to it. Start from the directory the scripts are in. | |
| 176 | +- **The file has a mistake in it.** The menu shows a greyed-out `Cannot read tools` where the commands would be, and **Create tools file** is still there. | |
| 177 | +- **`gogolo` or `wagolo` is not installed.** The popup shows `command not found` and `— exit 127`, which is what a shell would have said. GoloScript's `install.sh` installs all three binaries together; a release download is one binary at a time. | |
| 178 | +- **You want a menu named after one that exists.** `menu = "File"` gives you a second File menu, further along the bar, with a different hot key. Nothing stops you; nothing recommends it either. | |
| 179 | +- **Your menu has no hot key.** Every letter in its name was already taken. `F10` and the arrow keys reach it, and so does the mouse. Rename it to something with a free letter. | |
| 180 | +- **You misspelt `output`'s value.** The whole file is refused and the menu says `Cannot read tools`, naming the tool and listing what it could have been. A silent fallback would have sent the output somewhere you did not ask for. | |
| 181 | + | |
| 182 | +## Ask for a value when the command runs | |
| 183 | + | |
| 184 | +Six of the starter commands already do. The pattern is a `{{label}}` where the value goes: | |
| 185 | + | |
| 186 | +```toml | |
| 187 | +[[tool]] | |
| 188 | +name = "~N~ew script" | |
| 189 | +command = "golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}" | |
| 190 | +output = "popup" | |
| 191 | +``` | |
| 192 | + | |
| 193 | +Choosing it opens a box titled **New script** with two fields, one per placeholder, in the order they appear. **Tab** moves between them, **Enter** runs the command. Escape, and nothing runs. | |
| 194 | + | |
| 195 | +The value is quoted, so a path with a space in it stays one argument. | |
| 196 | + | |
| 197 | +### One field standing for several arguments | |
| 198 | + | |
| 199 | +Quoting is wrong when you mean "put these on the end". Add `...` inside the braces and the value goes in verbatim: | |
| 200 | + | |
| 201 | +```toml | |
| 202 | +[[tool]] | |
| 203 | +name = "Run with ~a~rguments" | |
| 204 | +command = "golo main.golo {{arguments...}}" | |
| 205 | +output = "terminal" | |
| 206 | +``` | |
| 207 | + | |
| 208 | +Type `--verbose input.txt` and both reach the script as separate arguments — `args` in `function main = |args|` holds them. | |
| 209 | + | |
| 210 | +### The same value twice | |
| 211 | + | |
| 212 | +Write the label twice; you are asked once: | |
| 213 | + | |
| 214 | +```toml | |
| 215 | +[[tool]] | |
| 216 | +name = "~C~ompile and run" | |
| 217 | +command = "gogolo build -o /tmp/app {{script}} && /tmp/app" | |
| 218 | +``` | |
| 219 | + | |
| 220 | +### Variants | |
| 221 | + | |
| 222 | +- **The value is the same most times.** Run it once and the box remembers what you typed, for the rest of the session. It is not written to disk. | |
| 223 | +- **Your command has braces in it already.** `awk '{print $1}'` and `find . -exec rm {} +` are left alone: only double braces ask for anything. | |
| 224 | +- **The command asks for more values than fit on screen.** The editor says so rather than opening a box whose OK button is below the bottom of the terminal. Make the terminal taller, or split the command into two tools. | |
| 225 | + | |
| 226 | +## See also | |
| 227 | + | |
| 228 | +- Every key of the file and every rule: [Golo tools reference](../reference/golo-tools.md) | |
| 229 | +- Why Run comes first, and why an unmodified file reloads: [Golo tools](../explanation/golo-tools.md) | |
| 230 | +- The windows the commands run in: [Terminal windows](../reference/terminal.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,230 @@ | |||
| 1 | +# How to run Golo commands from the editor | ||
| 2 | + | ||
| 3 | +This guide shows how to run, test, debug and compile your scripts without leaving Turbo Golo. It assumes the editor is installed and you have a directory with a `.golo` file in it. | ||
| 4 | + | ||
| 5 | +## Get a starter file | ||
| 6 | + | ||
| 7 | +Start the editor **from the directory your scripts live in**, then choose **Golo ▸ Create tools file** (`Alt-G`, then `C`). | ||
| 8 | + | ||
| 9 | +That writes `.turbo-golo/tools.toml` with the commands a Golo programmer runs most, and opens it. The first three: | ||
| 10 | + | ||
| 11 | +```toml | ||
| 12 | +[[tool]] | ||
| 13 | +name = "~R~un" | ||
| 14 | +# The interpreter, on the file you name. A terminal, not a popup: a script that | ||
| 15 | +# reads the keyboard has to be able to be answered, and one that runs long has | ||
| 16 | +# to be able to be interrupted. | ||
| 17 | +command = "golo {{script, e.g. main.golo}}" | ||
| 18 | +output = "terminal" | ||
| 19 | + | ||
| 20 | +[[tool]] | ||
| 21 | +name = "~T~est" | ||
| 22 | +# Every *_test.golo under the current directory, with gololang.Testing. | ||
| 23 | +command = "golo --test" | ||
| 24 | +output = "popup" | ||
| 25 | + | ||
| 26 | +[[tool]] | ||
| 27 | +name = "Test ~o~ne" | ||
| 28 | +command = "golo --test {{test file or directory}}" | ||
| 29 | +output = "popup" | ||
| 30 | +``` | ||
| 31 | + | ||
| 32 | +Each `[[tool]]` becomes one line of the **Golo** menu, in the order they appear — unless it names a `menu` of its own, which a later section covers. The file is read every time the menu opens, so an edit takes effect immediately. | ||
| 33 | + | ||
| 34 | +## Run one | ||
| 35 | + | ||
| 36 | +`Alt-G`, then the letter between the tildes — `R` to run, `T` to test. | ||
| 37 | + | ||
| 38 | +**Run** asks first, because Golo has no manifest that says which file is the program: | ||
| 39 | + | ||
| 40 | +``` | ||
| 41 | +┌──────────────── Run ────────────────┐ | ||
| 42 | +│ script, e.g. main.golo │ | ||
| 43 | +│ [ ] │ | ||
| 44 | +└─────────────────────────────────────┘ | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +Type `main.golo` and press `Enter`. A terminal window opens and the script runs in it; when it ends the window stays, showing what it printed. Press `Ctrl-W` to close it. Run it again and the box remembers the name for the rest of the session. | ||
| 48 | + | ||
| 49 | +**Test** opens a **popup** at once, which fills in as the command runs. Its title carries the command and, once it has ended, how it went: | ||
| 50 | + | ||
| 51 | +``` | ||
| 52 | +┌──────────────── golo --test — ok ─────────────────┐ | ||
| 53 | +│ 🧪 Running Golo tests... │ | ||
| 54 | +│ │ | ||
| 55 | +│ 📝 shapes_test.golo │ | ||
| 56 | +│ ✓ a point describes itself │ | ||
| 57 | +│ ✅ 1 test(s) passed │ | ||
| 58 | +│ │ | ||
| 59 | +│ [ Close ] │ | ||
| 60 | +└────────────────────────────────────────────────────┘ | ||
| 61 | +``` | ||
| 62 | + | ||
| 63 | +| Key | Effect | | ||
| 64 | +| --- | --- | | ||
| 65 | +| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Read through the output | | ||
| 66 | +| `Escape` | Close it — and **stop the command** if it is still running | | ||
| 67 | +| `Enter` | Close it | | ||
| 68 | + | ||
| 69 | +A command that succeeded silently shows `(no output)` rather than a blank box, so you can tell it from one that has not started. | ||
| 70 | + | ||
| 71 | +## The rest of the menu | ||
| 72 | + | ||
| 73 | +| Item | What it runs | Where | | ||
| 74 | +| --- | --- | --- | | ||
| 75 | +| **Debug** | `golo --debug <script>` — the interpreter with its step debugger on | a terminal, because the debugger reads the keyboard | | ||
| 76 | +| **REPL** | `golo` — the read-eval-print loop | a terminal | | ||
| 77 | +| **New script** | `golo new main --module <name> --name <file>` — a starter program from GoloScript's own template | a popup; the new file appears in the project tree | | ||
| 78 | +| **Build native** | `gogolo build -o <out> <script>` — Golo → Go → a native executable | a popup; needs the Go toolchain | | ||
| 79 | +| **Build wasm** | `wagolo build -target=<wasi\|js\|wasip2> -o <out.wasm> <script>` — Golo → Go → TinyGo → WebAssembly | a popup; needs TinyGo, and `wasm-tools` for `wasip2` | | ||
| 80 | + | ||
| 81 | +`Build native` can take a while: it runs the Go compiler. The popup is modal, so while it runs you cannot type anywhere else; `Escape` closes it and stops the compile. | ||
| 82 | + | ||
| 83 | +## Choose where the output goes | ||
| 84 | + | ||
| 85 | +Set `output` on a tool: | ||
| 86 | + | ||
| 87 | +| `output` | What you get | | ||
| 88 | +| --- | --- | | ||
| 89 | +| `popup` | A dialog that fills in as it runs. The default. | | ||
| 90 | +| `terminal` | A terminal window: colours, `Ctrl-C`, and the keyboard reaches the program | | ||
| 91 | +| `editor` | An editing window once it has finished, to search with `Ctrl-F` | | ||
| 92 | + | ||
| 93 | +`Run`, `Debug` and `REPL` are `terminal` in the starter file, and they are the example of why the key exists: a popup cannot answer a script that calls `readln`, cannot be interrupted with `Ctrl-C` while `httpServe` is listening, and cannot be a REPL at all. | ||
| 94 | + | ||
| 95 | +Reach for `editor` when the output is something to work through — the Go source that `gogolo transpile main.golo` prints, or a long test report you want to search. | ||
| 96 | + | ||
| 97 | +## A long command holds the editor | ||
| 98 | + | ||
| 99 | +A popup is modal: while `gogolo build` runs, you cannot type anywhere else. `Escape` closes it and stops the command. | ||
| 100 | + | ||
| 101 | +If that gets in the way for a particular command, give it `output = "terminal"` — the window is an ordinary one and you can carry on working beside it. That is what making the key configurable is for. | ||
| 102 | + | ||
| 103 | +## What happens to your open files | ||
| 104 | + | ||
| 105 | +Golo has no formatter, so nothing in the starter file rewrites the file you are looking at. But `New script` writes a new file into the directory, `gogolo build -keep-go` leaves a `.go` beside your script, and a tool of your own may do anything. When a command finishes, the editor **re-reads every open file that has no unsaved changes**, so a file another command changed appears as it now is, and the project tree is refreshed so a new one shows up. The status bar says how many. | ||
| 106 | + | ||
| 107 | +A file with unsaved changes is **left alone**, and the status bar says so too: | ||
| 108 | + | ||
| 109 | +``` | ||
| 110 | +Reloaded 2 files; 1 file with unsaved changes left alone | ||
| 111 | +``` | ||
| 112 | + | ||
| 113 | +That is deliberate: your edit and the command genuinely disagree, and the editor is not the one that should decide which wins. Save first (`F2`) and run the command again, or keep editing. | ||
| 114 | + | ||
| 115 | +## Add your own commands | ||
| 116 | + | ||
| 117 | +Edit `.turbo-golo/tools.toml`. A command goes to `sh -c`, so one entry can be a whole sequence: | ||
| 118 | + | ||
| 119 | +```toml | ||
| 120 | +[[tool]] | ||
| 121 | +name = "Test and ~b~uild" | ||
| 122 | +command = "golo --test && gogolo build -o bin/app main.golo" | ||
| 123 | +output = "popup" | ||
| 124 | + | ||
| 125 | +[[tool]] | ||
| 126 | +name = "~T~ranspile" | ||
| 127 | +command = "gogolo transpile {{script, e.g. main.golo}}" | ||
| 128 | +output = "editor" | ||
| 129 | + | ||
| 130 | +[[tool]] | ||
| 131 | +name = "Run in ~D~ocker" | ||
| 132 | +command = "docker run --rm -v \"$PWD:/app\" -w /app k33g/gololang:latest /golo ./{{script}}" | ||
| 133 | +output = "terminal" | ||
| 134 | +``` | ||
| 135 | + | ||
| 136 | +Give each a hot key with tildes, and keep them distinct — the menu answers the first match it finds. | ||
| 137 | + | ||
| 138 | +## Put a tool in a menu of its own | ||
| 139 | + | ||
| 140 | +A tool that has nothing to do with Golo does not belong in the Golo menu. Give it a `menu`: | ||
| 141 | + | ||
| 142 | +```toml | ||
| 143 | +[[tool]] | ||
| 144 | +name = "~E~cho" | ||
| 145 | +command = "echo TADA" | ||
| 146 | +output = "terminal" | ||
| 147 | +menu = "Tools" | ||
| 148 | + | ||
| 149 | +[[tool]] | ||
| 150 | +name = "~U~p" | ||
| 151 | +command = "docker compose up -d" | ||
| 152 | +menu = "Docker" | ||
| 153 | + | ||
| 154 | +[[tool]] | ||
| 155 | +name = "~D~own" | ||
| 156 | +command = "docker compose down" | ||
| 157 | +menu = "Docker" | ||
| 158 | +``` | ||
| 159 | + | ||
| 160 | +That gives you a **Tools** menu and a **Docker** menu on the bar, between Golo and Help, in the order the names first appear in the file. Docker holds both its tools. Nothing needs restarting: save the file and the bar follows. | ||
| 161 | + | ||
| 162 | +The name is yours to choose — there is no list to pick from. Leave `menu` out and the tool stays in Golo, which is where eight of the nine starter commands are. | ||
| 163 | + | ||
| 164 | +### The hot key is chosen for you | ||
| 165 | + | ||
| 166 | +You cannot know, when writing the file, which letters the editor's own menus have taken. So it works it out: the first letter of the name that nothing else claims gets the tildes. | ||
| 167 | + | ||
| 168 | +`Tools` gets `Alt-T`, because `T` is free. A menu called `Format` would get `Alt-A`, because `F` is File's, `o` is Options' and `r` is Run's. A menu called `Go` would get `Alt-O`… no — `O` is Options'; it would get no hot key at all, because `G` is Golo's, and `F10` would be the way to it. | ||
| 169 | + | ||
| 170 | +Write the tildes yourself — `menu = "Doc~k~er"` — and a free letter is kept. A taken one is not: the bar answers the *first* menu matching a key, so honouring your choice would make one of the two menus unreachable. It picks another letter and says nothing. | ||
| 171 | + | ||
| 172 | +## Variants | ||
| 173 | + | ||
| 174 | +- **You have one script and never another.** Replace `golo {{script, e.g. main.golo}}` with `golo main.golo`, and the box stops appearing. The placeholder is there because a starter file cannot know which file is the program. | ||
| 175 | +- **You started the editor from a subdirectory.** Commands run there, and relative paths in the box are relative to it. Start from the directory the scripts are in. | ||
| 176 | +- **The file has a mistake in it.** The menu shows a greyed-out `Cannot read tools` where the commands would be, and **Create tools file** is still there. | ||
| 177 | +- **`gogolo` or `wagolo` is not installed.** The popup shows `command not found` and `— exit 127`, which is what a shell would have said. GoloScript's `install.sh` installs all three binaries together; a release download is one binary at a time. | ||
| 178 | +- **You want a menu named after one that exists.** `menu = "File"` gives you a second File menu, further along the bar, with a different hot key. Nothing stops you; nothing recommends it either. | ||
| 179 | +- **Your menu has no hot key.** Every letter in its name was already taken. `F10` and the arrow keys reach it, and so does the mouse. Rename it to something with a free letter. | ||
| 180 | +- **You misspelt `output`'s value.** The whole file is refused and the menu says `Cannot read tools`, naming the tool and listing what it could have been. A silent fallback would have sent the output somewhere you did not ask for. | ||
| 181 | + | ||
| 182 | +## Ask for a value when the command runs | ||
| 183 | + | ||
| 184 | +Six of the starter commands already do. The pattern is a `{{label}}` where the value goes: | ||
| 185 | + | ||
| 186 | +```toml | ||
| 187 | +[[tool]] | ||
| 188 | +name = "~N~ew script" | ||
| 189 | +command = "golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}" | ||
| 190 | +output = "popup" | ||
| 191 | +``` | ||
| 192 | + | ||
| 193 | +Choosing it opens a box titled **New script** with two fields, one per placeholder, in the order they appear. **Tab** moves between them, **Enter** runs the command. Escape, and nothing runs. | ||
| 194 | + | ||
| 195 | +The value is quoted, so a path with a space in it stays one argument. | ||
| 196 | + | ||
| 197 | +### One field standing for several arguments | ||
| 198 | + | ||
| 199 | +Quoting is wrong when you mean "put these on the end". Add `...` inside the braces and the value goes in verbatim: | ||
| 200 | + | ||
| 201 | +```toml | ||
| 202 | +[[tool]] | ||
| 203 | +name = "Run with ~a~rguments" | ||
| 204 | +command = "golo main.golo {{arguments...}}" | ||
| 205 | +output = "terminal" | ||
| 206 | +``` | ||
| 207 | + | ||
| 208 | +Type `--verbose input.txt` and both reach the script as separate arguments — `args` in `function main = |args|` holds them. | ||
| 209 | + | ||
| 210 | +### The same value twice | ||
| 211 | + | ||
| 212 | +Write the label twice; you are asked once: | ||
| 213 | + | ||
| 214 | +```toml | ||
| 215 | +[[tool]] | ||
| 216 | +name = "~C~ompile and run" | ||
| 217 | +command = "gogolo build -o /tmp/app {{script}} && /tmp/app" | ||
| 218 | +``` | ||
| 219 | + | ||
| 220 | +### Variants | ||
| 221 | + | ||
| 222 | +- **The value is the same most times.** Run it once and the box remembers what you typed, for the rest of the session. It is not written to disk. | ||
| 223 | +- **Your command has braces in it already.** `awk '{print $1}'` and `find . -exec rm {} +` are left alone: only double braces ask for anything. | ||
| 224 | +- **The command asks for more values than fit on screen.** The editor says so rather than opening a box whose OK button is below the bottom of the terminal. Make the terminal taller, or split the command into two tools. | ||
| 225 | + | ||
| 226 | +## See also | ||
| 227 | + | ||
| 228 | +- Every key of the file and every rule: [Golo tools reference](../reference/golo-tools.md) | ||
| 229 | +- Why Run comes first, and why an unmodified file reloads: [Golo tools](../explanation/golo-tools.md) | ||
| 230 | +- The windows the commands run in: [Terminal windows](../reference/terminal.md) | ||
added
docs/en/how-to/run-the-tests.md +97 -0 | new file mode 100644 | ||
| @@ -0,0 +1,97 @@ | ||
| 1 | +# How to run the tests | |
| 2 | + | |
| 3 | +This guide shows how to run and read Turbo Golo's test suite. It assumes you have a checkout and Go 1.26 or later. | |
| 4 | + | |
| 5 | +## The whole suite | |
| 6 | + | |
| 7 | +```bash | |
| 8 | +make test | |
| 9 | +``` | |
| 10 | + | |
| 11 | +That is the single documented command. It runs `go test ./...` across every package. | |
| 12 | + | |
| 13 | +## Variants | |
| 14 | + | |
| 15 | +**See each test by name:** | |
| 16 | + | |
| 17 | +```bash | |
| 18 | +make test-verbose | |
| 19 | +``` | |
| 20 | + | |
| 21 | +**Measure coverage per package:** | |
| 22 | + | |
| 23 | +```bash | |
| 24 | +make cover | |
| 25 | +``` | |
| 26 | + | |
| 27 | +**One package only:** | |
| 28 | + | |
| 29 | +```bash | |
| 30 | +go test ./internal/gololang/ | |
| 31 | +``` | |
| 32 | + | |
| 33 | +**Without starting a language server, and without building the editor.** The tests in `internal/gololang/editor_test.go` whose names end in `WithRealGoloLSP` start a real `golo lsp` when they find one, and the installer tests at the root compile the whole editor. To skip both: | |
| 34 | + | |
| 35 | +```bash | |
| 36 | +go test -short ./... | |
| 37 | +``` | |
| 38 | + | |
| 39 | +**With the race detector.** The LSP client is concurrent, so this is worth running before touching anything that talks to it: | |
| 40 | + | |
| 41 | +```bash | |
| 42 | +go test -race ./... | |
| 43 | +``` | |
| 44 | + | |
| 45 | +**Everything a commit should pass:** | |
| 46 | + | |
| 47 | +```bash | |
| 48 | +make check | |
| 49 | +``` | |
| 50 | + | |
| 51 | +This runs `go fmt`, `go vet` and the tests, in that order. | |
| 52 | + | |
| 53 | +## What the suite covers | |
| 54 | + | |
| 55 | +No test needs a real terminal. `internal/gololang/editor_test.go` assembles a whole Turbo Golo on tcell's `SimulationScreen` — a real `Screen` that draws into memory — opens a file and checks the colouring, the menu bar and the hot keys on the picture a terminal would actually show. `scan_test.go` holds the scanner to every construct it colours and every one it must refuse — `0xFF` as a number, a leading dot, `---` closing a block comment. | |
| 56 | + | |
| 57 | +The tests named `…WithRealGoloLSP` start the real interpreter in language-server mode: they complete text that exists only in the buffer, go to a definition, read a hover, list a file's symbols, find the references and the implementation of a call, search the project for a symbol, and wait for a diagnostic on a file that does not parse and on one with a C-style comment. Two more pin what the toolchain is: `TestTheScannersTablesMatchWhatTheServerOffers` checks the scanner's keyword and builtin tables against what `golo lsp` actually offers, and `TestGoloLSPDoesNotAnswerTypeDefinitionWithRealGoloLSP` fails the day a future `golo` starts answering type definitions — so the documentation gets revisited rather than quietly going stale, as it was when GoloScript v0.2.0 started answering references, implementations and project-wide symbols. | |
| 58 | + | |
| 59 | +All of them **skip themselves** when `golo` is not installed, and under `-short`, so a checkout without the interpreter still has a green suite. | |
| 60 | + | |
| 61 | +## Testing against an unreleased turbo-core | |
| 62 | + | |
| 63 | +Most of Turbo Golo is turbo-core, and this repository depends on it by version, from the module proxy: | |
| 64 | + | |
| 65 | +``` | |
| 66 | +require rickub.com/turbo-editors/turbo-core v0.5.0 | |
| 67 | +``` | |
| 68 | + | |
| 69 | +A change made in a turbo-core checkout beside this one is therefore invisible here until it is published. To test it before that, make a workspace: | |
| 70 | + | |
| 71 | +```bash | |
| 72 | +go work init . ../turbo-core | |
| 73 | +make test | |
| 74 | +``` | |
| 75 | + | |
| 76 | +Every import of the library now resolves to that checkout. Nothing in `go.mod` or `go.sum` changes, so there is no edit to undo. Check it took effect — this is the mistake worth guarding against, because everything still builds and still passes if it did not: | |
| 77 | + | |
| 78 | +```bash | |
| 79 | +go list -f '{{.Dir}}' rickub.com/turbo-editors/turbo-core/app | |
| 80 | +``` | |
| 81 | + | |
| 82 | +The answer should be your checkout, not a path under `pkg/mod`. When you are done, `rm go.work go.work.sum`; both are gitignored, so they cannot be committed by accident. | |
| 83 | + | |
| 84 | +## Code quality | |
| 85 | + | |
| 86 | +The test suite is not the whole gate. Quality is measured separately: | |
| 87 | + | |
| 88 | +```bash | |
| 89 | +python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace . | |
| 90 | +``` | |
| 91 | + | |
| 92 | +It writes a report under `.quality/` and exits non-zero if the gate fails. | |
| 93 | + | |
| 94 | +## See also | |
| 95 | + | |
| 96 | +- Why the tests are shaped this way: [Architecture](../explanation/architecture.md) | |
| 97 | +- Every make target: [command line reference](../reference/cli.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,97 @@ | |||
| 1 | +# How to run the tests | ||
| 2 | + | ||
| 3 | +This guide shows how to run and read Turbo Golo's test suite. It assumes you have a checkout and Go 1.26 or later. | ||
| 4 | + | ||
| 5 | +## The whole suite | ||
| 6 | + | ||
| 7 | +```bash | ||
| 8 | +make test | ||
| 9 | +``` | ||
| 10 | + | ||
| 11 | +That is the single documented command. It runs `go test ./...` across every package. | ||
| 12 | + | ||
| 13 | +## Variants | ||
| 14 | + | ||
| 15 | +**See each test by name:** | ||
| 16 | + | ||
| 17 | +```bash | ||
| 18 | +make test-verbose | ||
| 19 | +``` | ||
| 20 | + | ||
| 21 | +**Measure coverage per package:** | ||
| 22 | + | ||
| 23 | +```bash | ||
| 24 | +make cover | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +**One package only:** | ||
| 28 | + | ||
| 29 | +```bash | ||
| 30 | +go test ./internal/gololang/ | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +**Without starting a language server, and without building the editor.** The tests in `internal/gololang/editor_test.go` whose names end in `WithRealGoloLSP` start a real `golo lsp` when they find one, and the installer tests at the root compile the whole editor. To skip both: | ||
| 34 | + | ||
| 35 | +```bash | ||
| 36 | +go test -short ./... | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +**With the race detector.** The LSP client is concurrent, so this is worth running before touching anything that talks to it: | ||
| 40 | + | ||
| 41 | +```bash | ||
| 42 | +go test -race ./... | ||
| 43 | +``` | ||
| 44 | + | ||
| 45 | +**Everything a commit should pass:** | ||
| 46 | + | ||
| 47 | +```bash | ||
| 48 | +make check | ||
| 49 | +``` | ||
| 50 | + | ||
| 51 | +This runs `go fmt`, `go vet` and the tests, in that order. | ||
| 52 | + | ||
| 53 | +## What the suite covers | ||
| 54 | + | ||
| 55 | +No test needs a real terminal. `internal/gololang/editor_test.go` assembles a whole Turbo Golo on tcell's `SimulationScreen` — a real `Screen` that draws into memory — opens a file and checks the colouring, the menu bar and the hot keys on the picture a terminal would actually show. `scan_test.go` holds the scanner to every construct it colours and every one it must refuse — `0xFF` as a number, a leading dot, `---` closing a block comment. | ||
| 56 | + | ||
| 57 | +The tests named `…WithRealGoloLSP` start the real interpreter in language-server mode: they complete text that exists only in the buffer, go to a definition, read a hover, list a file's symbols, find the references and the implementation of a call, search the project for a symbol, and wait for a diagnostic on a file that does not parse and on one with a C-style comment. Two more pin what the toolchain is: `TestTheScannersTablesMatchWhatTheServerOffers` checks the scanner's keyword and builtin tables against what `golo lsp` actually offers, and `TestGoloLSPDoesNotAnswerTypeDefinitionWithRealGoloLSP` fails the day a future `golo` starts answering type definitions — so the documentation gets revisited rather than quietly going stale, as it was when GoloScript v0.2.0 started answering references, implementations and project-wide symbols. | ||
| 58 | + | ||
| 59 | +All of them **skip themselves** when `golo` is not installed, and under `-short`, so a checkout without the interpreter still has a green suite. | ||
| 60 | + | ||
| 61 | +## Testing against an unreleased turbo-core | ||
| 62 | + | ||
| 63 | +Most of Turbo Golo is turbo-core, and this repository depends on it by version, from the module proxy: | ||
| 64 | + | ||
| 65 | +``` | ||
| 66 | +require rickub.com/turbo-editors/turbo-core v0.5.0 | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +A change made in a turbo-core checkout beside this one is therefore invisible here until it is published. To test it before that, make a workspace: | ||
| 70 | + | ||
| 71 | +```bash | ||
| 72 | +go work init . ../turbo-core | ||
| 73 | +make test | ||
| 74 | +``` | ||
| 75 | + | ||
| 76 | +Every import of the library now resolves to that checkout. Nothing in `go.mod` or `go.sum` changes, so there is no edit to undo. Check it took effect — this is the mistake worth guarding against, because everything still builds and still passes if it did not: | ||
| 77 | + | ||
| 78 | +```bash | ||
| 79 | +go list -f '{{.Dir}}' rickub.com/turbo-editors/turbo-core/app | ||
| 80 | +``` | ||
| 81 | + | ||
| 82 | +The answer should be your checkout, not a path under `pkg/mod`. When you are done, `rm go.work go.work.sum`; both are gitignored, so they cannot be committed by accident. | ||
| 83 | + | ||
| 84 | +## Code quality | ||
| 85 | + | ||
| 86 | +The test suite is not the whole gate. Quality is measured separately: | ||
| 87 | + | ||
| 88 | +```bash | ||
| 89 | +python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace . | ||
| 90 | +``` | ||
| 91 | + | ||
| 92 | +It writes a report under `.quality/` and exits non-zero if the gate fails. | ||
| 93 | + | ||
| 94 | +## See also | ||
| 95 | + | ||
| 96 | +- Why the tests are shaped this way: [Architecture](../explanation/architecture.md) | ||
| 97 | +- Every make target: [command line reference](../reference/cli.md) | ||
added
docs/en/how-to/talk-to-an-agent.md +177 -0 | new file mode 100644 | ||
| @@ -0,0 +1,177 @@ | ||
| 1 | +# How to talk to a coding agent from the editor | |
| 2 | + | |
| 3 | +This guide shows how to point Turbo Golo at an agent that speaks the [Agent Client Protocol](https://agentclientprotocol.com), open a window onto it, and hold a conversation about the code you are editing. It assumes you already have Turbo Golo running in a project. | |
| 4 | + | |
| 5 | +Turbo Golo is an ACP **client**. It starts the agent as a child process and talks JSON-RPC to it over its standard input and output — the same arrangement Zed uses, so an agent that works there works here. | |
| 6 | + | |
| 7 | +## Tell the editor about an agent | |
| 8 | + | |
| 9 | +Agents are listed in `acp.toml`. Choose **Agent ▸ Create agents file** and the editor writes a starter one into `.turbo-gololo/acp.toml` and opens it. | |
| 10 | + | |
| 11 | +An agent is one `[[agent]]` block: | |
| 12 | + | |
| 13 | +```toml | |
| 14 | +[[agent]] | |
| 15 | +name = "Bob (llama.cpp)" | |
| 16 | +command = "docker" | |
| 17 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | |
| 18 | +env = { TELEMETRY_ENABLED = "false" } | |
| 19 | +``` | |
| 20 | + | |
| 21 | +`name` is what the Agent menu shows and what the window is called. `command` and `args` are how the agent is started. That is the whole of it — the file is read again every time you open a window, so you never restart the editor to try a change. | |
| 22 | + | |
| 23 | +List as many as you like. Each becomes its own line in the menu, and each window you open from it is a separate process with a conversation of its own. | |
| 24 | + | |
| 25 | +## Put the agent's own configuration beside it | |
| 26 | + | |
| 27 | +Most agents have a configuration file of their own, and `.turbo-gololo/` is a reasonable place to keep it so that it travels with the project. For `docker agent`, saving this as `.turbo-gololo/agent.yaml` is what the `args` above point at: | |
| 28 | + | |
| 29 | +```yaml | |
| 30 | +providers: | |
| 31 | + llamacpp: | |
| 32 | + api_type: openai_chatcompletions | |
| 33 | + base_url: http://localhost:8080/v1 | |
| 34 | + | |
| 35 | +models: | |
| 36 | + mellum2: | |
| 37 | + provider: llamacpp | |
| 38 | + model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M | |
| 39 | + temperature: 0.7 | |
| 40 | + provider_opts: | |
| 41 | + context_size: 262144 | |
| 42 | + | |
| 43 | +agents: | |
| 44 | + root: | |
| 45 | + model: mellum2 | |
| 46 | + description: A helpful AI assistant running on a local llama.cpp server | |
| 47 | + instruction: | | |
| 48 | + You name is Bob 🤓, you are a knowledgeable code assistant. | |
| 49 | + Be helpful, accurate, and concise in your responses. | |
| 50 | + You have access to the local filesystem and shell: use these tools | |
| 51 | + toolsets: | |
| 52 | + - type: filesystem | |
| 53 | + - type: shell | |
| 54 | +``` | |
| 55 | + | |
| 56 | +## Open a window on it | |
| 57 | + | |
| 58 | +Press `Alt-A`, or choose **Agent** from the menu bar, and pick the agent by name. | |
| 59 | + | |
| 60 | +A window opens, split in two: the conversation above, and a box to type in below. The agent is started when the window opens and stopped when it closes. | |
| 61 | + | |
| 62 | +``` | |
| 63 | +┌ Bob (llama.cpp) ───────────────────────────────[■]┐ | |
| 64 | +│ ‣ You │ | |
| 65 | +│ What does buildMenus do? │ | |
| 66 | +│ │ | |
| 67 | +│ ‣ Shell ls -1 internal/ ✓ done │ | |
| 68 | +│ golang │ | |
| 69 | +│ │ | |
| 70 | +│ ‣ Bob │ | |
| 71 | +│ It assembles the menu bar. Here is the shape: │ | |
| 72 | +│ │ | |
| 73 | +│ ```golo │ | |
| 74 | +│ func (a *App) buildMenus() *ui.MenuBar { │ | |
| 75 | +│ return ui.NewMenuBar(a.allMenus()...) │ | |
| 76 | +│ } │ | |
| 77 | +│ ``` │ | |
| 78 | +├───────────────────────────────────────────────────┤ | |
| 79 | +│ > _ │ | |
| 80 | +└───────────────────────────────────────────────────┘ | |
| 81 | +``` | |
| 82 | + | |
| 83 | +Code the agent sends inside a fenced block is coloured by the same scanners the editor uses for files, so a Golo answer is coloured as Golo and a shell answer as shell. A fence naming a language the editor does not colour is left plain rather than guessed at. | |
| 84 | + | |
| 85 | +## Hold the conversation | |
| 86 | + | |
| 87 | +| Key | Effect | | |
| 88 | +| --- | --- | | |
| 89 | +| `Enter` | Send what you have typed | | |
| 90 | +| `Alt-Enter` | Start a new line instead of sending | | |
| 91 | +| `Tab` | Move between the conversation and the input box | | |
| 92 | +| `PgUp` `PgDn` | Scroll the conversation a screenful at a time | | |
| 93 | +| `Esc` | Stop the turn in progress | | |
| 94 | +| `Ctrl-W` | Close the window, and stop the agent with it | | |
| 95 | + | |
| 96 | +While the agent is answering, its reply appears as it is written rather than all at once, and the rule between the two panes turns a spinner beside the word *thinking*. `Esc` interrupts it — the agent is told to stop, and what it had already said stays in the window. | |
| 97 | + | |
| 98 | +## Use the agent's own commands | |
| 99 | + | |
| 100 | +Some agents answer to commands — `/compact`, `/web`, `/plan` — and tell the editor which ones. Type `/` as the first character of the box and the list opens over the conversation: each command, what it does, and in angle brackets what it wants after its name. | |
| 101 | + | |
| 102 | +Type on to narrow it, `↑` `↓` to move, then `Tab` to complete. A command that takes something is completed with a space after it, ready for you to type the rest; press `Enter` when the line is what you mean. If nothing appears when you type `/`, the agent announced no commands — **Agent ▸ Agent status** says so — and `/` is only a character. | |
| 103 | + | |
| 104 | +## Point the agent at a file | |
| 105 | + | |
| 106 | +Type `@` anywhere in the box and the project's files appear. Type a few letters of the file's name to narrow the list, `Tab` to take the highlighted one: | |
| 107 | + | |
| 108 | +``` | |
| 109 | +> explain what @internal/scanner.go does | |
| 110 | +``` | |
| 111 | + | |
| 112 | +When you press `Enter`, the agent is given the **file**, not merely its name: its text when the agent accepts embedded context, a link to it otherwise. If the file is open in the editor with unsaved changes, it is your unsaved version that goes. The line stays in the conversation as you typed it. | |
| 113 | + | |
| 114 | +Several files in one prompt is several `@`. A word that begins with `@` but is not a file — an e-mail address — is left as text. | |
| 115 | + | |
| 116 | +## Take something out of the conversation | |
| 117 | + | |
| 118 | +Press `Tab` to put the cursor in the conversation. The rule changes to say what the keys now do. | |
| 119 | + | |
| 120 | +| Key | Effect | | |
| 121 | +| --- | --- | | |
| 122 | +| `↑` `↓` `PgUp` `PgDn` | Move the cursor through what was said | | |
| 123 | +| `Shift-↑` `Shift-↓` | Select whole lines | | |
| 124 | +| Drag with the mouse | The same, by hand | | |
| 125 | +| `Ctrl-C` | Copy | | |
| 126 | +| `Esc` | Drop the selection | | |
| 127 | +| `Tab` | Back to the box | | |
| 128 | + | |
| 129 | +**With nothing selected, `Ctrl-C` copies the block the cursor is on** — one fenced code block, one paragraph, one tool's output — without the speaker's label above it and without the sentence after it. That is almost always what you wanted, and it saves selecting it by hand. | |
| 130 | + | |
| 131 | +What is copied goes to **two** clipboards: this editor's, so `Shift-Ins` pastes it into a file you have open, and your system's, so `Ctrl-V` pastes it anywhere else. The indentation the conversation is drawn with is taken off, so pasted code lands flush against the margin. | |
| 132 | + | |
| 133 | +The system half travels through your terminal (an escape sequence called OSC 52). Most terminals do it; a few refuse it for security, and some need it turned on. If `Ctrl-V` elsewhere gives you nothing, that is where to look — the editor's own clipboard has the text either way. | |
| 134 | + | |
| 135 | +## Answer the agent when it asks permission | |
| 136 | + | |
| 137 | +An agent with a shell or a filesystem toolset asks before it uses one. A dialog names the tool and the exact command, and offers the choices the agent itself proposed — normally *Allow this action*, *Allow and remember my choice*, and *Skip this action*. | |
| 138 | + | |
| 139 | +``` | |
| 140 | +┌───────────── Bob (llama.cpp) wants to run ─────────────┐ | |
| 141 | +│ │ | |
| 142 | +│ Shell │ | |
| 143 | +│ ls -1 │ | |
| 144 | +│ │ | |
| 145 | +│ [ Allow ] [ Allow always ] [ Skip ] │ | |
| 146 | +└────────────────────────────────────────────────────────┘ | |
| 147 | +``` | |
| 148 | + | |
| 149 | +*Allow always* is remembered by the agent, not by the editor, so what it covers and how long it lasts are the agent's business. Escape is the same answer as *Skip*. | |
| 150 | + | |
| 151 | +Nothing runs before you answer. An agent waiting on a permission dialog is simply blocked, which is the point. | |
| 152 | + | |
| 153 | +## Let the agent see what you have not saved yet | |
| 154 | + | |
| 155 | +The editor offers the agent its own filesystem: when the agent reads a file you have open with unsaved changes, it is given **the text in the buffer**, not the older text on disk. That is usually what you want — you are asking about the edit you just made. | |
| 156 | + | |
| 157 | +When the agent writes a file, the change lands in the buffer and the window is marked modified, so you can read it, undo it with `Ctrl-Z`, or save it with `F2`. A file you do not have open is read from and written to disk directly. | |
| 158 | + | |
| 159 | +## Run several agents at once | |
| 160 | + | |
| 161 | +Each window is its own process and its own conversation. Opening the same agent twice gives two independent sessions, and opening two different agents lets you put a fast local model and a slower careful one side by side — **Window ▸ Tile** arranges them. | |
| 162 | + | |
| 163 | +Leaving the editor stops every agent. | |
| 164 | + | |
| 165 | +## Variants | |
| 166 | + | |
| 167 | +- **You want the agent to run somewhere other than the project root.** Add `cwd = "backend"` to its block. The path is relative to the project, and is both where the process starts and what the agent is told the working directory is. | |
| 168 | +- **The agent needs a credential.** Put it in `env`, or rely on it being in the environment you started the editor from — the agent inherits it. | |
| 169 | +- **The agent's commands do not appear when you type `/`.** Open **Agent ▸ Agent status** with the window in front. If it lists no commands, the agent announced none — or announced them in a shape this editor could not read, in which case the dialog names the update and the decoding error. To see exactly what went over the wire, start the editor with `TURBO_ACP_TRACE=/tmp/acp.log` and read the file: `->` is what the editor sent, `<-` what the agent answered. | |
| 170 | +- **The agent will not start.** **Agent ▸ Agent status** lists what was read from `acp.toml`, what each agent's command line came out as, and the error from anything that failed to start. Whatever the agent writes to its standard error is shown there too, which is where a misconfigured model endpoint reports itself. | |
| 171 | +- **You keep the same agent in every project.** Put the `[[agent]]` block in `~/.config/turbo-golo/acp.toml` instead. A project's own file is read afterwards and an agent with the same `name` in it replaces yours. | |
| 172 | + | |
| 173 | +## See also | |
| 174 | + | |
| 175 | +- Every key of the file, and exactly how much of the protocol is implemented: [Agents and ACP reference](../reference/acp.md) | |
| 176 | +- Why an agent is a window rather than a panel, and why permissions are modal: [Agent windows](../explanation/agent-windows.md) | |
| 177 | +- The protocol itself: [agentclientprotocol.com](https://agentclientprotocol.com) | |
| new file mode 100644 | |||
| @@ -0,0 +1,177 @@ | |||
| 1 | +# How to talk to a coding agent from the editor | ||
| 2 | + | ||
| 3 | +This guide shows how to point Turbo Golo at an agent that speaks the [Agent Client Protocol](https://agentclientprotocol.com), open a window onto it, and hold a conversation about the code you are editing. It assumes you already have Turbo Golo running in a project. | ||
| 4 | + | ||
| 5 | +Turbo Golo is an ACP **client**. It starts the agent as a child process and talks JSON-RPC to it over its standard input and output — the same arrangement Zed uses, so an agent that works there works here. | ||
| 6 | + | ||
| 7 | +## Tell the editor about an agent | ||
| 8 | + | ||
| 9 | +Agents are listed in `acp.toml`. Choose **Agent ▸ Create agents file** and the editor writes a starter one into `.turbo-gololo/acp.toml` and opens it. | ||
| 10 | + | ||
| 11 | +An agent is one `[[agent]]` block: | ||
| 12 | + | ||
| 13 | +```toml | ||
| 14 | +[[agent]] | ||
| 15 | +name = "Bob (llama.cpp)" | ||
| 16 | +command = "docker" | ||
| 17 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | ||
| 18 | +env = { TELEMETRY_ENABLED = "false" } | ||
| 19 | +``` | ||
| 20 | + | ||
| 21 | +`name` is what the Agent menu shows and what the window is called. `command` and `args` are how the agent is started. That is the whole of it — the file is read again every time you open a window, so you never restart the editor to try a change. | ||
| 22 | + | ||
| 23 | +List as many as you like. Each becomes its own line in the menu, and each window you open from it is a separate process with a conversation of its own. | ||
| 24 | + | ||
| 25 | +## Put the agent's own configuration beside it | ||
| 26 | + | ||
| 27 | +Most agents have a configuration file of their own, and `.turbo-gololo/` is a reasonable place to keep it so that it travels with the project. For `docker agent`, saving this as `.turbo-gololo/agent.yaml` is what the `args` above point at: | ||
| 28 | + | ||
| 29 | +```yaml | ||
| 30 | +providers: | ||
| 31 | + llamacpp: | ||
| 32 | + api_type: openai_chatcompletions | ||
| 33 | + base_url: http://localhost:8080/v1 | ||
| 34 | + | ||
| 35 | +models: | ||
| 36 | + mellum2: | ||
| 37 | + provider: llamacpp | ||
| 38 | + model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M | ||
| 39 | + temperature: 0.7 | ||
| 40 | + provider_opts: | ||
| 41 | + context_size: 262144 | ||
| 42 | + | ||
| 43 | +agents: | ||
| 44 | + root: | ||
| 45 | + model: mellum2 | ||
| 46 | + description: A helpful AI assistant running on a local llama.cpp server | ||
| 47 | + instruction: | | ||
| 48 | + You name is Bob 🤓, you are a knowledgeable code assistant. | ||
| 49 | + Be helpful, accurate, and concise in your responses. | ||
| 50 | + You have access to the local filesystem and shell: use these tools | ||
| 51 | + toolsets: | ||
| 52 | + - type: filesystem | ||
| 53 | + - type: shell | ||
| 54 | +``` | ||
| 55 | + | ||
| 56 | +## Open a window on it | ||
| 57 | + | ||
| 58 | +Press `Alt-A`, or choose **Agent** from the menu bar, and pick the agent by name. | ||
| 59 | + | ||
| 60 | +A window opens, split in two: the conversation above, and a box to type in below. The agent is started when the window opens and stopped when it closes. | ||
| 61 | + | ||
| 62 | +``` | ||
| 63 | +┌ Bob (llama.cpp) ───────────────────────────────[■]┐ | ||
| 64 | +│ ‣ You │ | ||
| 65 | +│ What does buildMenus do? │ | ||
| 66 | +│ │ | ||
| 67 | +│ ‣ Shell ls -1 internal/ ✓ done │ | ||
| 68 | +│ golang │ | ||
| 69 | +│ │ | ||
| 70 | +│ ‣ Bob │ | ||
| 71 | +│ It assembles the menu bar. Here is the shape: │ | ||
| 72 | +│ │ | ||
| 73 | +│ ```golo │ | ||
| 74 | +│ func (a *App) buildMenus() *ui.MenuBar { │ | ||
| 75 | +│ return ui.NewMenuBar(a.allMenus()...) │ | ||
| 76 | +│ } │ | ||
| 77 | +│ ``` │ | ||
| 78 | +├───────────────────────────────────────────────────┤ | ||
| 79 | +│ > _ │ | ||
| 80 | +└───────────────────────────────────────────────────┘ | ||
| 81 | +``` | ||
| 82 | + | ||
| 83 | +Code the agent sends inside a fenced block is coloured by the same scanners the editor uses for files, so a Golo answer is coloured as Golo and a shell answer as shell. A fence naming a language the editor does not colour is left plain rather than guessed at. | ||
| 84 | + | ||
| 85 | +## Hold the conversation | ||
| 86 | + | ||
| 87 | +| Key | Effect | | ||
| 88 | +| --- | --- | | ||
| 89 | +| `Enter` | Send what you have typed | | ||
| 90 | +| `Alt-Enter` | Start a new line instead of sending | | ||
| 91 | +| `Tab` | Move between the conversation and the input box | | ||
| 92 | +| `PgUp` `PgDn` | Scroll the conversation a screenful at a time | | ||
| 93 | +| `Esc` | Stop the turn in progress | | ||
| 94 | +| `Ctrl-W` | Close the window, and stop the agent with it | | ||
| 95 | + | ||
| 96 | +While the agent is answering, its reply appears as it is written rather than all at once, and the rule between the two panes turns a spinner beside the word *thinking*. `Esc` interrupts it — the agent is told to stop, and what it had already said stays in the window. | ||
| 97 | + | ||
| 98 | +## Use the agent's own commands | ||
| 99 | + | ||
| 100 | +Some agents answer to commands — `/compact`, `/web`, `/plan` — and tell the editor which ones. Type `/` as the first character of the box and the list opens over the conversation: each command, what it does, and in angle brackets what it wants after its name. | ||
| 101 | + | ||
| 102 | +Type on to narrow it, `↑` `↓` to move, then `Tab` to complete. A command that takes something is completed with a space after it, ready for you to type the rest; press `Enter` when the line is what you mean. If nothing appears when you type `/`, the agent announced no commands — **Agent ▸ Agent status** says so — and `/` is only a character. | ||
| 103 | + | ||
| 104 | +## Point the agent at a file | ||
| 105 | + | ||
| 106 | +Type `@` anywhere in the box and the project's files appear. Type a few letters of the file's name to narrow the list, `Tab` to take the highlighted one: | ||
| 107 | + | ||
| 108 | +``` | ||
| 109 | +> explain what @internal/scanner.go does | ||
| 110 | +``` | ||
| 111 | + | ||
| 112 | +When you press `Enter`, the agent is given the **file**, not merely its name: its text when the agent accepts embedded context, a link to it otherwise. If the file is open in the editor with unsaved changes, it is your unsaved version that goes. The line stays in the conversation as you typed it. | ||
| 113 | + | ||
| 114 | +Several files in one prompt is several `@`. A word that begins with `@` but is not a file — an e-mail address — is left as text. | ||
| 115 | + | ||
| 116 | +## Take something out of the conversation | ||
| 117 | + | ||
| 118 | +Press `Tab` to put the cursor in the conversation. The rule changes to say what the keys now do. | ||
| 119 | + | ||
| 120 | +| Key | Effect | | ||
| 121 | +| --- | --- | | ||
| 122 | +| `↑` `↓` `PgUp` `PgDn` | Move the cursor through what was said | | ||
| 123 | +| `Shift-↑` `Shift-↓` | Select whole lines | | ||
| 124 | +| Drag with the mouse | The same, by hand | | ||
| 125 | +| `Ctrl-C` | Copy | | ||
| 126 | +| `Esc` | Drop the selection | | ||
| 127 | +| `Tab` | Back to the box | | ||
| 128 | + | ||
| 129 | +**With nothing selected, `Ctrl-C` copies the block the cursor is on** — one fenced code block, one paragraph, one tool's output — without the speaker's label above it and without the sentence after it. That is almost always what you wanted, and it saves selecting it by hand. | ||
| 130 | + | ||
| 131 | +What is copied goes to **two** clipboards: this editor's, so `Shift-Ins` pastes it into a file you have open, and your system's, so `Ctrl-V` pastes it anywhere else. The indentation the conversation is drawn with is taken off, so pasted code lands flush against the margin. | ||
| 132 | + | ||
| 133 | +The system half travels through your terminal (an escape sequence called OSC 52). Most terminals do it; a few refuse it for security, and some need it turned on. If `Ctrl-V` elsewhere gives you nothing, that is where to look — the editor's own clipboard has the text either way. | ||
| 134 | + | ||
| 135 | +## Answer the agent when it asks permission | ||
| 136 | + | ||
| 137 | +An agent with a shell or a filesystem toolset asks before it uses one. A dialog names the tool and the exact command, and offers the choices the agent itself proposed — normally *Allow this action*, *Allow and remember my choice*, and *Skip this action*. | ||
| 138 | + | ||
| 139 | +``` | ||
| 140 | +┌───────────── Bob (llama.cpp) wants to run ─────────────┐ | ||
| 141 | +│ │ | ||
| 142 | +│ Shell │ | ||
| 143 | +│ ls -1 │ | ||
| 144 | +│ │ | ||
| 145 | +│ [ Allow ] [ Allow always ] [ Skip ] │ | ||
| 146 | +└────────────────────────────────────────────────────────┘ | ||
| 147 | +``` | ||
| 148 | + | ||
| 149 | +*Allow always* is remembered by the agent, not by the editor, so what it covers and how long it lasts are the agent's business. Escape is the same answer as *Skip*. | ||
| 150 | + | ||
| 151 | +Nothing runs before you answer. An agent waiting on a permission dialog is simply blocked, which is the point. | ||
| 152 | + | ||
| 153 | +## Let the agent see what you have not saved yet | ||
| 154 | + | ||
| 155 | +The editor offers the agent its own filesystem: when the agent reads a file you have open with unsaved changes, it is given **the text in the buffer**, not the older text on disk. That is usually what you want — you are asking about the edit you just made. | ||
| 156 | + | ||
| 157 | +When the agent writes a file, the change lands in the buffer and the window is marked modified, so you can read it, undo it with `Ctrl-Z`, or save it with `F2`. A file you do not have open is read from and written to disk directly. | ||
| 158 | + | ||
| 159 | +## Run several agents at once | ||
| 160 | + | ||
| 161 | +Each window is its own process and its own conversation. Opening the same agent twice gives two independent sessions, and opening two different agents lets you put a fast local model and a slower careful one side by side — **Window ▸ Tile** arranges them. | ||
| 162 | + | ||
| 163 | +Leaving the editor stops every agent. | ||
| 164 | + | ||
| 165 | +## Variants | ||
| 166 | + | ||
| 167 | +- **You want the agent to run somewhere other than the project root.** Add `cwd = "backend"` to its block. The path is relative to the project, and is both where the process starts and what the agent is told the working directory is. | ||
| 168 | +- **The agent needs a credential.** Put it in `env`, or rely on it being in the environment you started the editor from — the agent inherits it. | ||
| 169 | +- **The agent's commands do not appear when you type `/`.** Open **Agent ▸ Agent status** with the window in front. If it lists no commands, the agent announced none — or announced them in a shape this editor could not read, in which case the dialog names the update and the decoding error. To see exactly what went over the wire, start the editor with `TURBO_ACP_TRACE=/tmp/acp.log` and read the file: `->` is what the editor sent, `<-` what the agent answered. | ||
| 170 | +- **The agent will not start.** **Agent ▸ Agent status** lists what was read from `acp.toml`, what each agent's command line came out as, and the error from anything that failed to start. Whatever the agent writes to its standard error is shown there too, which is where a misconfigured model endpoint reports itself. | ||
| 171 | +- **You keep the same agent in every project.** Put the `[[agent]]` block in `~/.config/turbo-golo/acp.toml` instead. A project's own file is read afterwards and an agent with the same `name` in it replaces yours. | ||
| 172 | + | ||
| 173 | +## See also | ||
| 174 | + | ||
| 175 | +- Every key of the file, and exactly how much of the protocol is implemented: [Agents and ACP reference](../reference/acp.md) | ||
| 176 | +- Why an agent is a window rather than a panel, and why permissions are modal: [Agent windows](../explanation/agent-windows.md) | ||
| 177 | +- The protocol itself: [agentclientprotocol.com](https://agentclientprotocol.com) | ||
added
docs/en/how-to/use-a-terminal.md +68 -0 | new file mode 100644 | ||
| @@ -0,0 +1,68 @@ | ||
| 1 | +# How to run shell commands without leaving the editor | |
| 2 | + | |
| 3 | +This guide shows how to open a terminal window, run and test the script you are editing in it, and get back to the file. It assumes you already have Turbo Golo running with a file open. | |
| 4 | + | |
| 5 | +## Open a terminal | |
| 6 | + | |
| 7 | +Press `F8`, or choose **Window ▸ New terminal**. | |
| 8 | + | |
| 9 | +A new window opens running your shell, in the directory of the file you were editing. That is normally the directory you want: `golo --test` and `git diff` both act on the directory you are looking at. | |
| 10 | + | |
| 11 | +The window is called after the shell, and renames itself when a program inside it sets a title — `vim`, `htop` and `ssh` all do. | |
| 12 | + | |
| 13 | +## Run something | |
| 14 | + | |
| 15 | +Type into it as you would into any terminal. The shell gets nearly every key, including the ones the editor would otherwise use: `Ctrl-C` interrupts, `Ctrl-W` deletes a word, `Ctrl-R` searches the history. | |
| 16 | + | |
| 17 | +```bash | |
| 18 | +golo main.golo | |
| 19 | +golo --test | |
| 20 | +golo # the REPL — a real terminal, so it can read your keyboard | |
| 21 | +``` | |
| 22 | + | |
| 23 | +What the editor keeps is short, and deliberate — it is the way back out: | |
| 24 | + | |
| 25 | +| Key | Effect, even with a terminal in front | | |
| 26 | +| --- | --- | | |
| 27 | +| `F8` | Open another terminal | | |
| 28 | +| `F6` | Move to the next window | | |
| 29 | +| `F10` | Open the menu bar | | |
| 30 | +| `F2` `F3` `F4` | Save, Open, New | | |
| 31 | +| `Alt-1` … `Alt-9` | Bring that window forward | | |
| 32 | +| `Alt-X` | Leave the editor | | |
| 33 | + | |
| 34 | +## Read back through what scrolled off | |
| 35 | + | |
| 36 | +`Shift-PgUp` and `Shift-PgDn` walk the history a screenful at a time; the mouse wheel moves three lines. Two thousand lines are kept. | |
| 37 | + | |
| 38 | +Typing anything brings you straight back to the live screen, so you never have to scroll back down before running the next command. | |
| 39 | + | |
| 40 | +## Work with the file and the shell side by side | |
| 41 | + | |
| 42 | +A terminal is an ordinary window, so the window commands all apply to it: | |
| 43 | + | |
| 44 | +- **Window ▸ Tile** puts the file and the terminal side by side. | |
| 45 | +- **Window ▸ Maximise**, or the `[■]` box at the right of its title bar, gives the terminal the whole desktop while a build runs. The box then reads `[▬]`, and pressing it puts the window back. | |
| 46 | +- Drag its bottom-right corner to resize it — the shell is told its new size, so `less` and `vim` reflow. | |
| 47 | + | |
| 48 | +## Close it | |
| 49 | + | |
| 50 | +`Ctrl-W` is the shell's, not the editor's, so closing a terminal is done another way: | |
| 51 | + | |
| 52 | +- **File ▸ Close**, or | |
| 53 | +- click the `[x]` box in its top-left corner. | |
| 54 | + | |
| 55 | +Either ends the shell running in it. Nothing is asked first: a terminal holds a running process, not unsaved work, and closing the window is how you say you have finished with it. Leaving the editor closes every terminal at once. | |
| 56 | + | |
| 57 | +## Variants | |
| 58 | + | |
| 59 | +- **You want a different shell.** The shell is taken from `$SHELL`, falling back to `/bin/sh`; on Windows from `%COMSPEC%`, falling back to `cmd.exe`. Start the editor with `SHELL=/bin/zsh turbo-golo` to change it for that run. | |
| 60 | +- **No file is open.** The terminal starts in the directory the editor was started from. | |
| 61 | +- **You run the same command every time.** Put it in the project's tools file instead, and it becomes a line of the **Golo** menu — with a terminal window as its output when it needs the keyboard. See [How to run Golo commands from the editor](run-golo-commands.md). | |
| 62 | +- **You are on Windows.** Terminal windows run in a pseudo-console (ConPTY), which needs Windows 10 version 1809 or later, and the shell is `%COMSPEC%` — cmd.exe. This path has been built and vetted but not yet run by the authors, who work on Linux and macOS. The first time you use it, try the five things it has to get right — `F8`, type `dir`, resize the window, run a menu command that says `output = "terminal"`, and interrupt a long one with `Ctrl-C` — and report whatever did not behave. | |
| 63 | + | |
| 64 | +## See also | |
| 65 | + | |
| 66 | +- Everything the terminal implements, exactly: [Terminal windows reference](../reference/terminal.md) | |
| 67 | +- Why it runs a real shell rather than capturing command output: [Terminal windows](../explanation/terminal-windows.md) | |
| 68 | +- The colours it uses: [Theme file format](../reference/themes.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,68 @@ | |||
| 1 | +# How to run shell commands without leaving the editor | ||
| 2 | + | ||
| 3 | +This guide shows how to open a terminal window, run and test the script you are editing in it, and get back to the file. It assumes you already have Turbo Golo running with a file open. | ||
| 4 | + | ||
| 5 | +## Open a terminal | ||
| 6 | + | ||
| 7 | +Press `F8`, or choose **Window ▸ New terminal**. | ||
| 8 | + | ||
| 9 | +A new window opens running your shell, in the directory of the file you were editing. That is normally the directory you want: `golo --test` and `git diff` both act on the directory you are looking at. | ||
| 10 | + | ||
| 11 | +The window is called after the shell, and renames itself when a program inside it sets a title — `vim`, `htop` and `ssh` all do. | ||
| 12 | + | ||
| 13 | +## Run something | ||
| 14 | + | ||
| 15 | +Type into it as you would into any terminal. The shell gets nearly every key, including the ones the editor would otherwise use: `Ctrl-C` interrupts, `Ctrl-W` deletes a word, `Ctrl-R` searches the history. | ||
| 16 | + | ||
| 17 | +```bash | ||
| 18 | +golo main.golo | ||
| 19 | +golo --test | ||
| 20 | +golo # the REPL — a real terminal, so it can read your keyboard | ||
| 21 | +``` | ||
| 22 | + | ||
| 23 | +What the editor keeps is short, and deliberate — it is the way back out: | ||
| 24 | + | ||
| 25 | +| Key | Effect, even with a terminal in front | | ||
| 26 | +| --- | --- | | ||
| 27 | +| `F8` | Open another terminal | | ||
| 28 | +| `F6` | Move to the next window | | ||
| 29 | +| `F10` | Open the menu bar | | ||
| 30 | +| `F2` `F3` `F4` | Save, Open, New | | ||
| 31 | +| `Alt-1` … `Alt-9` | Bring that window forward | | ||
| 32 | +| `Alt-X` | Leave the editor | | ||
| 33 | + | ||
| 34 | +## Read back through what scrolled off | ||
| 35 | + | ||
| 36 | +`Shift-PgUp` and `Shift-PgDn` walk the history a screenful at a time; the mouse wheel moves three lines. Two thousand lines are kept. | ||
| 37 | + | ||
| 38 | +Typing anything brings you straight back to the live screen, so you never have to scroll back down before running the next command. | ||
| 39 | + | ||
| 40 | +## Work with the file and the shell side by side | ||
| 41 | + | ||
| 42 | +A terminal is an ordinary window, so the window commands all apply to it: | ||
| 43 | + | ||
| 44 | +- **Window ▸ Tile** puts the file and the terminal side by side. | ||
| 45 | +- **Window ▸ Maximise**, or the `[■]` box at the right of its title bar, gives the terminal the whole desktop while a build runs. The box then reads `[▬]`, and pressing it puts the window back. | ||
| 46 | +- Drag its bottom-right corner to resize it — the shell is told its new size, so `less` and `vim` reflow. | ||
| 47 | + | ||
| 48 | +## Close it | ||
| 49 | + | ||
| 50 | +`Ctrl-W` is the shell's, not the editor's, so closing a terminal is done another way: | ||
| 51 | + | ||
| 52 | +- **File ▸ Close**, or | ||
| 53 | +- click the `[x]` box in its top-left corner. | ||
| 54 | + | ||
| 55 | +Either ends the shell running in it. Nothing is asked first: a terminal holds a running process, not unsaved work, and closing the window is how you say you have finished with it. Leaving the editor closes every terminal at once. | ||
| 56 | + | ||
| 57 | +## Variants | ||
| 58 | + | ||
| 59 | +- **You want a different shell.** The shell is taken from `$SHELL`, falling back to `/bin/sh`; on Windows from `%COMSPEC%`, falling back to `cmd.exe`. Start the editor with `SHELL=/bin/zsh turbo-golo` to change it for that run. | ||
| 60 | +- **No file is open.** The terminal starts in the directory the editor was started from. | ||
| 61 | +- **You run the same command every time.** Put it in the project's tools file instead, and it becomes a line of the **Golo** menu — with a terminal window as its output when it needs the keyboard. See [How to run Golo commands from the editor](run-golo-commands.md). | ||
| 62 | +- **You are on Windows.** Terminal windows run in a pseudo-console (ConPTY), which needs Windows 10 version 1809 or later, and the shell is `%COMSPEC%` — cmd.exe. This path has been built and vetted but not yet run by the authors, who work on Linux and macOS. The first time you use it, try the five things it has to get right — `F8`, type `dir`, resize the window, run a menu command that says `output = "terminal"`, and interrupt a long one with `Ctrl-C` — and report whatever did not behave. | ||
| 63 | + | ||
| 64 | +## See also | ||
| 65 | + | ||
| 66 | +- Everything the terminal implements, exactly: [Terminal windows reference](../reference/terminal.md) | ||
| 67 | +- Why it runs a real shell rather than capturing command output: [Terminal windows](../explanation/terminal-windows.md) | ||
| 68 | +- The colours it uses: [Theme file format](../reference/themes.md) | ||
added
docs/en/how-to/use-snippets.md +101 -0 | new file mode 100644 | ||
| @@ -0,0 +1,101 @@ | ||
| 1 | +# How to insert snippets from a menu | |
| 2 | + | |
| 3 | +This guide shows how to set up reusable pieces of text and put them into a file at the cursor. It assumes Turbo Golo is already installed. | |
| 4 | + | |
| 5 | +## Get a starter file | |
| 6 | + | |
| 7 | +Start the editor **from the project's own directory**, then choose **Snippets ▸ Create snippets file** (`Alt-N`, then `C`). | |
| 8 | + | |
| 9 | +That writes `.turbo-golo/snippets.toml`, filled in with a dozen Golo constructs and a couple of general ones, and opens it — coloured, because Turbo Golo colours TOML: | |
| 10 | + | |
| 11 | +```toml | |
| 12 | +[[snippet]] | |
| 13 | +name = "main" | |
| 14 | +group = "Golo" | |
| 15 | +languages = ["golo"] | |
| 16 | +body = ''' | |
| 17 | +function main = |args| { | |
| 18 | + println("Hello, Golo!") | |
| 19 | +}''' | |
| 20 | + | |
| 21 | +[[snippet]] | |
| 22 | +group = "General" | |
| 23 | +name = "Hello" | |
| 24 | +body = "Hello!!!" | |
| 25 | +``` | |
| 26 | + | |
| 27 | +Each `[[snippet]]` becomes one line of the menu. The file is read every time the menu opens, so editing it takes effect immediately — no restart. | |
| 28 | + | |
| 29 | +## Insert one | |
| 30 | + | |
| 31 | +Open **Snippets** (`Alt-N`). Snippets sharing a `group` appear together in a submenu of that name; one with no group goes into **General**. | |
| 32 | + | |
| 33 | +| Key | Effect | | |
| 34 | +| --- | --- | | |
| 35 | +| `Alt-N`, or `F10` then `→` to Snippets | Open the menu | | |
| 36 | +| `↑` `↓` | Move down the groups | | |
| 37 | +| `→`, or `Enter` | Open the highlighted group | | |
| 38 | +| `↑` `↓` then `Enter` | Insert the highlighted snippet | | |
| 39 | +| `←` | Back out of a group | | |
| 40 | +| `Escape` | Put the whole menu away | | |
| 41 | + | |
| 42 | +The snippet goes in at the cursor. **Lines after the first are indented to match the line you inserted it on**, so a multi-line snippet dropped into a nested block lands where you would have typed it: | |
| 43 | + | |
| 44 | +``` | |
| 45 | +function main = |args| { | |
| 46 | + | ← cursor here | |
| 47 | +} | |
| 48 | +``` | |
| 49 | + | |
| 50 | +becomes, after **Golo ▸ foreach**, | |
| 51 | + | |
| 52 | +``` | |
| 53 | +function main = |args| { | |
| 54 | + foreach item in list[1, 2, 3] { | |
| 55 | + println(item) | |
| 56 | + } | |
| 57 | +} | |
| 58 | +``` | |
| 59 | + | |
| 60 | +It is one undo step: `Ctrl-Z` takes the whole snippet back out. | |
| 61 | + | |
| 62 | +## Keep snippets across every project | |
| 63 | + | |
| 64 | +Put them in `~/.config/turbo-golo/snippets.toml` — the same directory your own themes go in. Those appear in every project, and a project's own file adds to them rather than replacing them. | |
| 65 | + | |
| 66 | +Where a project and you use the same `name` in the same `group`, **the project's wins**: it is the more specific statement of the two. | |
| 67 | + | |
| 68 | +## Show a snippet only where it makes sense | |
| 69 | + | |
| 70 | +Add `languages`, using the names the editor uses — `golo`, `toml`, `markdown`, `javascript`, `html`, `bash`: | |
| 71 | + | |
| 72 | +```toml | |
| 73 | +[[snippet]] | |
| 74 | +name = "strict mode" | |
| 75 | +group = "Shell" | |
| 76 | +languages = ["bash"] | |
| 77 | +body = "set -euo pipefail" | |
| 78 | +``` | |
| 79 | + | |
| 80 | +That snippet then appears only when a shell script is the front window. Leave `languages` out and the snippet is offered everywhere, which is what you want for a licence header or a `TODO`. | |
| 81 | + | |
| 82 | +A group left with nothing after filtering does not appear at all. | |
| 83 | + | |
| 84 | +## Write a Golo body | |
| 85 | + | |
| 86 | +Two habits, both taken from the starter file: | |
| 87 | + | |
| 88 | +- **Indent with two spaces.** It is what every example in the GoloScript documentation uses, and Golo has no formatter to disagree with, so it is the only convention there is. | |
| 89 | +- **Use a literal string** — `'''…'''`, single quotes — for any body with a backslash in it. A Golo string carries `\n` and `\"` the way a Go string does, and in a TOML basic string (`"""…"""`) those escapes are resolved before the editor sees them: `println("caught: \"" + e + "\"")` would arrive with real quotation marks in it and no longer parse. Inside `'''` a backslash is just a backslash. | |
| 90 | + | |
| 91 | +## Variants | |
| 92 | + | |
| 93 | +- **You started the editor from a subdirectory.** The project's file is not found: only `./.turbo-golo` is looked at, the same rule `settings.toml` follows. Your own snippets still appear. | |
| 94 | +- **The file has a mistake in it.** The menu shows a greyed-out `Cannot read snippets` where the groups would be, and **Create snippets file** is still there. Open the file and fix it. | |
| 95 | +- **You want a tab in a body.** In a basic string, write `\t` and TOML turns it into a tab when it reads the file. In a literal string, `\t` stays two characters — type the tab itself. | |
| 96 | + | |
| 97 | +## See also | |
| 98 | + | |
| 99 | +- Every key of the file and every rule: [Snippets reference](../reference/snippets.md) | |
| 100 | +- Why the menu is rebuilt each time it opens, why insertion re-indents, and why the Golo bodies are literal strings: [Snippets](../explanation/snippets.md) | |
| 101 | +- The other file in `.turbo-golo`: [Project settings](../reference/project-settings.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,101 @@ | |||
| 1 | +# How to insert snippets from a menu | ||
| 2 | + | ||
| 3 | +This guide shows how to set up reusable pieces of text and put them into a file at the cursor. It assumes Turbo Golo is already installed. | ||
| 4 | + | ||
| 5 | +## Get a starter file | ||
| 6 | + | ||
| 7 | +Start the editor **from the project's own directory**, then choose **Snippets ▸ Create snippets file** (`Alt-N`, then `C`). | ||
| 8 | + | ||
| 9 | +That writes `.turbo-golo/snippets.toml`, filled in with a dozen Golo constructs and a couple of general ones, and opens it — coloured, because Turbo Golo colours TOML: | ||
| 10 | + | ||
| 11 | +```toml | ||
| 12 | +[[snippet]] | ||
| 13 | +name = "main" | ||
| 14 | +group = "Golo" | ||
| 15 | +languages = ["golo"] | ||
| 16 | +body = ''' | ||
| 17 | +function main = |args| { | ||
| 18 | + println("Hello, Golo!") | ||
| 19 | +}''' | ||
| 20 | + | ||
| 21 | +[[snippet]] | ||
| 22 | +group = "General" | ||
| 23 | +name = "Hello" | ||
| 24 | +body = "Hello!!!" | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +Each `[[snippet]]` becomes one line of the menu. The file is read every time the menu opens, so editing it takes effect immediately — no restart. | ||
| 28 | + | ||
| 29 | +## Insert one | ||
| 30 | + | ||
| 31 | +Open **Snippets** (`Alt-N`). Snippets sharing a `group` appear together in a submenu of that name; one with no group goes into **General**. | ||
| 32 | + | ||
| 33 | +| Key | Effect | | ||
| 34 | +| --- | --- | | ||
| 35 | +| `Alt-N`, or `F10` then `→` to Snippets | Open the menu | | ||
| 36 | +| `↑` `↓` | Move down the groups | | ||
| 37 | +| `→`, or `Enter` | Open the highlighted group | | ||
| 38 | +| `↑` `↓` then `Enter` | Insert the highlighted snippet | | ||
| 39 | +| `←` | Back out of a group | | ||
| 40 | +| `Escape` | Put the whole menu away | | ||
| 41 | + | ||
| 42 | +The snippet goes in at the cursor. **Lines after the first are indented to match the line you inserted it on**, so a multi-line snippet dropped into a nested block lands where you would have typed it: | ||
| 43 | + | ||
| 44 | +``` | ||
| 45 | +function main = |args| { | ||
| 46 | + | ← cursor here | ||
| 47 | +} | ||
| 48 | +``` | ||
| 49 | + | ||
| 50 | +becomes, after **Golo ▸ foreach**, | ||
| 51 | + | ||
| 52 | +``` | ||
| 53 | +function main = |args| { | ||
| 54 | + foreach item in list[1, 2, 3] { | ||
| 55 | + println(item) | ||
| 56 | + } | ||
| 57 | +} | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +It is one undo step: `Ctrl-Z` takes the whole snippet back out. | ||
| 61 | + | ||
| 62 | +## Keep snippets across every project | ||
| 63 | + | ||
| 64 | +Put them in `~/.config/turbo-golo/snippets.toml` — the same directory your own themes go in. Those appear in every project, and a project's own file adds to them rather than replacing them. | ||
| 65 | + | ||
| 66 | +Where a project and you use the same `name` in the same `group`, **the project's wins**: it is the more specific statement of the two. | ||
| 67 | + | ||
| 68 | +## Show a snippet only where it makes sense | ||
| 69 | + | ||
| 70 | +Add `languages`, using the names the editor uses — `golo`, `toml`, `markdown`, `javascript`, `html`, `bash`: | ||
| 71 | + | ||
| 72 | +```toml | ||
| 73 | +[[snippet]] | ||
| 74 | +name = "strict mode" | ||
| 75 | +group = "Shell" | ||
| 76 | +languages = ["bash"] | ||
| 77 | +body = "set -euo pipefail" | ||
| 78 | +``` | ||
| 79 | + | ||
| 80 | +That snippet then appears only when a shell script is the front window. Leave `languages` out and the snippet is offered everywhere, which is what you want for a licence header or a `TODO`. | ||
| 81 | + | ||
| 82 | +A group left with nothing after filtering does not appear at all. | ||
| 83 | + | ||
| 84 | +## Write a Golo body | ||
| 85 | + | ||
| 86 | +Two habits, both taken from the starter file: | ||
| 87 | + | ||
| 88 | +- **Indent with two spaces.** It is what every example in the GoloScript documentation uses, and Golo has no formatter to disagree with, so it is the only convention there is. | ||
| 89 | +- **Use a literal string** — `'''…'''`, single quotes — for any body with a backslash in it. A Golo string carries `\n` and `\"` the way a Go string does, and in a TOML basic string (`"""…"""`) those escapes are resolved before the editor sees them: `println("caught: \"" + e + "\"")` would arrive with real quotation marks in it and no longer parse. Inside `'''` a backslash is just a backslash. | ||
| 90 | + | ||
| 91 | +## Variants | ||
| 92 | + | ||
| 93 | +- **You started the editor from a subdirectory.** The project's file is not found: only `./.turbo-golo` is looked at, the same rule `settings.toml` follows. Your own snippets still appear. | ||
| 94 | +- **The file has a mistake in it.** The menu shows a greyed-out `Cannot read snippets` where the groups would be, and **Create snippets file** is still there. Open the file and fix it. | ||
| 95 | +- **You want a tab in a body.** In a basic string, write `\t` and TOML turns it into a tab when it reads the file. In a literal string, `\t` stays two characters — type the tab itself. | ||
| 96 | + | ||
| 97 | +## See also | ||
| 98 | + | ||
| 99 | +- Every key of the file and every rule: [Snippets reference](../reference/snippets.md) | ||
| 100 | +- Why the menu is rebuilt each time it opens, why insertion re-indents, and why the Golo bodies are literal strings: [Snippets](../explanation/snippets.md) | ||
| 101 | +- The other file in `.turbo-golo`: [Project settings](../reference/project-settings.md) | ||
added
docs/en/how-to/write-a-theme.md +160 -0 | new file mode 100644 | ||
| @@ -0,0 +1,160 @@ | ||
| 1 | +# How to write your own theme | |
| 2 | + | |
| 3 | +This guide shows how to add a colour theme of your own. It assumes you know where your configuration directory is and can edit a TOML file. | |
| 4 | + | |
| 5 | +## 1. Find where themes go | |
| 6 | + | |
| 7 | +```bash | |
| 8 | +turbo-golo -list-themes | |
| 9 | +``` | |
| 10 | + | |
| 11 | +The last line tells you the directory — `~/.config/turbo-golo/themes` on Linux, `~/Library/Application Support/turbo-golo/themes` on macOS. Create it: | |
| 12 | + | |
| 13 | +```bash | |
| 14 | +mkdir -p ~/.config/turbo-golo/themes | |
| 15 | +``` | |
| 16 | + | |
| 17 | +## 2. Start from an existing theme | |
| 18 | + | |
| 19 | +The quickest start is to inherit from one that already works and override only what you want: | |
| 20 | + | |
| 21 | +```toml | |
| 22 | +# ~/.config/turbo-golo/themes/mine.toml | |
| 23 | +name = "Mine" | |
| 24 | +description = "Turbo Classic, but the comments are readable." | |
| 25 | +inherits = "turbo-classic" | |
| 26 | + | |
| 27 | +[colors] | |
| 28 | +"syntax.comment" = { fg = "#8a8a8a", italic = true } | |
| 29 | +"syntax.string" = { fg = "#87d7af" } | |
| 30 | +``` | |
| 31 | + | |
| 32 | +Everything you do not set is taken from `turbo-classic`. | |
| 33 | + | |
| 34 | +**Inherit from a theme whose ground is the same as yours.** The colours you leave out were chosen against the background of the theme you inherit from, so a dark theme built on `turbo-classic` will show, here and there, a colour picked for Borland navy. If you are writing a dark theme, inherit from `turbo-dark`, `cappuccino`, `catppuccin-frappe`, `cobalt`, `darcula` or `monochrome-dark`; if a light one, from `borland-light`, `catppuccin-latte`, `intellij-light` or `monochrome-light`. That is also why the eleven themes that ship in the binary each state their palette in full rather than inheriting most of it — a test holds them to it, because a shipped theme is one the project is answerable for. | |
| 35 | + | |
| 36 | +## 3. Use it | |
| 37 | + | |
| 38 | +```bash | |
| 39 | +turbo-golo -theme mine main.golo | |
| 40 | +``` | |
| 41 | + | |
| 42 | +Or from inside the editor: `Options ▸ Theme…`, which lists every theme it can find. | |
| 43 | + | |
| 44 | +## 4. Iterate | |
| 45 | + | |
| 46 | +Edit the file, then restart the editor. There is no live reload. | |
| 47 | + | |
| 48 | +If the theme fails to load, Turbo Golo falls back to the default rather than refusing to start. To see *why* it failed: | |
| 49 | + | |
| 50 | +```bash | |
| 51 | +turbo-golo -list-themes | |
| 52 | +``` | |
| 53 | + | |
| 54 | +A broken theme is listed with the parse error beside it — an unknown colour name is an error, not a silent fallback, so a typo is pointed at rather than quietly repainting half the screen. | |
| 55 | + | |
| 56 | +## 5. Check it stays readable | |
| 57 | + | |
| 58 | +turbo-core, the library the themes ship in, holds every one of them to five measured rules, and they are worth applying to your own. Its test suite runs them. | |
| 59 | + | |
| 60 | +| Rule | Why | | |
| 61 | +| --- | --- | | |
| 62 | +| The cursor is at least 64 apart from the line it sits on, in its strongest channel | A terminal draws its cursor over the cell; one that blends in cannot be found | | |
| 63 | +| The cursor is never a plain reversal of that line | A terminal that draws its cursor by inverting the cell would invert it back into invisibility | | |
| 64 | +| The current line is at least 16 from the page | `turbo-dark` once used ten, which is no highlight at all | | |
| 65 | +| Text you have to read is at least 64 from its background | Furniture — the desktop, a shadow, a scrollbar trough, a disabled entry — is exempt: it exists to recede | | |
| 66 | +| Comments read at 4.5:1 or better against their background, by WCAG relative luminance | A comment is prose, read word by word. `turbo-classic` drew them in `#808080` on its navy: 128 channel values apart, so the rule above waved it through, and 4.05:1 to read, which is below the W3C's floor for body text. | | |
| 67 | + | |
| 68 | +The contrast rule is applied to every shipped theme but the two Catppuccin ones, and the exemption is written where it is made: their colours are somebody else's published palette, faithfully copied, and Catppuccin puts comments at 2.87:1 in Frappé and 2.83:1 in Latte. A theme called Catppuccin that is not those exact values is a different theme wearing a borrowed name, so the fix — if anyone wants one — is upstream. | |
| 69 | + | |
| 70 | +It is applied to comments and to nothing else. `syntax.punctuation` is quieter still in several themes and stays that way: punctuation is recognised by shape, not read. | |
| 71 | + | |
| 72 | +A sixth rule catches the mistake no measurement finds: **two syntax classes a reader meets side by side must not be drawn identically**. `turbo-classic` once painted `syntax.link` the same lime as `syntax.string`, so a Markdown link and an inline code span were the same thing on screen — every colour readable, every key set, and the two simply equal. Both monochromes pass this rule with no hue at all, by using bold, italic and underline instead. | |
| 73 | + | |
| 74 | +## Variants | |
| 75 | + | |
| 76 | +**Override a shipped theme rather than adding one.** Name your file after it — `turbo-classic.toml` — and yours wins. The embedded one is not replaced, so deleting your file brings it back. | |
| 77 | + | |
| 78 | +**Start from scratch.** Leave `inherits` out. Set at least `default`; every key you do not set falls back along the dots to it, so a theme with one line is still a usable theme. | |
| 79 | + | |
| 80 | +**Colour only the syntax.** One key does it: | |
| 81 | + | |
| 82 | +```toml | |
| 83 | +[colors] | |
| 84 | +syntax = { fg = "silver" } | |
| 85 | +``` | |
| 86 | + | |
| 87 | +`syntax.keyword`, `syntax.string` and the rest all fall back to it. | |
| 88 | + | |
| 89 | +**Keep the terminal's own colours.** Use `default` as a colour value: | |
| 90 | + | |
| 91 | +```toml | |
| 92 | +[colors] | |
| 93 | +"editor.text" = { fg = "default", bg = "default" } | |
| 94 | +``` | |
| 95 | + | |
| 96 | +**Test it in a checkout without installing it.** Point the editor at any directory: | |
| 97 | + | |
| 98 | +```bash | |
| 99 | +TURBO_GOLO_THEME_DIR=./my-themes turbo-golo -theme mine main.golo | |
| 100 | +``` | |
| 101 | + | |
| 102 | +**The cursor is hard to see.** `editor.cursor` does two things: its **background** is sent to the terminal as the cursor's own colour, and it also paints the cell underneath as a fallback for terminals that ignore that. Set it to something loud: | |
| 103 | + | |
| 104 | +```toml | |
| 105 | +[colors] | |
| 106 | +"editor.cursor" = { fg = "#000000", bg = "#ff8700" } | |
| 107 | +``` | |
| 108 | + | |
| 109 | +Two things make a cursor colour a bad one, and the test suite rejects both: a plain reversal of the line — which terminals that draw their cursor by inverting the cell turn back into invisibility — and anything less than 64 channel values away from the line it sits on. | |
| 110 | + | |
| 111 | +**The cursor's line is hard to find.** That is `editor.currentline`, and it is a different key. It has to differ from `editor.text` by at least 16 channel values to count as a highlight at all. | |
| 112 | + | |
| 113 | +**Golo types and builtins look alike.** In `turbo-classic` a capitalised name such as `Point` is `syntax.type`, and `println` is `syntax.builtin`; both are aqua on navy, and only the builtin is bold. If that is too close for you, give one of them a colour of its own: | |
| 114 | + | |
| 115 | +```toml | |
| 116 | +[colors] | |
| 117 | +"syntax.type" = { fg = "yellow" } | |
| 118 | +"syntax.builtin" = { fg = "aqua", bold = true } | |
| 119 | +``` | |
| 120 | + | |
| 121 | +**Markdown and HTML look plain.** Five keys belong to the markup languages and have no equivalent in Golo, so a theme written before they existed does not set them: | |
| 122 | + | |
| 123 | +```toml | |
| 124 | +[colors] | |
| 125 | +"syntax.heading" = { fg = "white", bold = true } | |
| 126 | +"syntax.tag" = { fg = "aqua" } | |
| 127 | +"syntax.attribute" = { fg = "yellow" } | |
| 128 | +"syntax.emphasis" = { fg = "fuchsia", bold = true } | |
| 129 | +"syntax.link" = { fg = "aqua", underline = true } | |
| 130 | +``` | |
| 131 | + | |
| 132 | +Give `syntax.link` a different colour from `syntax.string`: a link and an inline `code` span sit side by side in most prose, and sharing a colour makes them one blur. The shipped `turbo-classic` had exactly that fault until it was looked at on a real terminal. | |
| 133 | + | |
| 134 | +**The project tree looks flat.** It has four keys of its own, and none of them falls back to `list`: | |
| 135 | + | |
| 136 | +```toml | |
| 137 | +[colors] | |
| 138 | +"tree.text" = { fg = "silver", bg = "navy" } | |
| 139 | +"tree.directory" = { fg = "white", bg = "navy", bold = true } | |
| 140 | +"tree.selected" = { fg = "black", bg = "aqua" } | |
| 141 | +"tree.unfocused" = { fg = "black", bg = "gray" } | |
| 142 | +``` | |
| 143 | + | |
| 144 | +Give `tree.text` the same background as `window.body`, so the tree looks like part of its window, and make `tree.selected` clearly different from it — the test suite holds every shipped theme to at least 64 channel values between the two, because a highlight the same colour as the page is no highlight. | |
| 145 | + | |
| 146 | +**Terminal windows look wrong.** They have two keys of their own, and neither falls back to `editor`: | |
| 147 | + | |
| 148 | +```toml | |
| 149 | +[colors] | |
| 150 | +"terminal.text" = { fg = "silver", bg = "black" } | |
| 151 | +"terminal.cursor" = { fg = "black", bg = "aqua" } | |
| 152 | +``` | |
| 153 | + | |
| 154 | +`terminal.text` is what a shell's output gets when it names no colour of its own — set it to something close to a real terminal rather than to your editor background, or `less` and `htop` will look out of place. A program that does name its colours keeps them either way. | |
| 155 | + | |
| 156 | +## See also | |
| 157 | + | |
| 158 | +- Every key you may set, and every colour name: [theme file reference](../reference/themes.md) | |
| 159 | +- Which class each piece of Golo gets: [Languages coloured](../reference/languages.md) | |
| 160 | +- Why the format is TOML with two kinds of inheritance: [Design decisions](../explanation/design-decisions.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,160 @@ | |||
| 1 | +# How to write your own theme | ||
| 2 | + | ||
| 3 | +This guide shows how to add a colour theme of your own. It assumes you know where your configuration directory is and can edit a TOML file. | ||
| 4 | + | ||
| 5 | +## 1. Find where themes go | ||
| 6 | + | ||
| 7 | +```bash | ||
| 8 | +turbo-golo -list-themes | ||
| 9 | +``` | ||
| 10 | + | ||
| 11 | +The last line tells you the directory — `~/.config/turbo-golo/themes` on Linux, `~/Library/Application Support/turbo-golo/themes` on macOS. Create it: | ||
| 12 | + | ||
| 13 | +```bash | ||
| 14 | +mkdir -p ~/.config/turbo-golo/themes | ||
| 15 | +``` | ||
| 16 | + | ||
| 17 | +## 2. Start from an existing theme | ||
| 18 | + | ||
| 19 | +The quickest start is to inherit from one that already works and override only what you want: | ||
| 20 | + | ||
| 21 | +```toml | ||
| 22 | +# ~/.config/turbo-golo/themes/mine.toml | ||
| 23 | +name = "Mine" | ||
| 24 | +description = "Turbo Classic, but the comments are readable." | ||
| 25 | +inherits = "turbo-classic" | ||
| 26 | + | ||
| 27 | +[colors] | ||
| 28 | +"syntax.comment" = { fg = "#8a8a8a", italic = true } | ||
| 29 | +"syntax.string" = { fg = "#87d7af" } | ||
| 30 | +``` | ||
| 31 | + | ||
| 32 | +Everything you do not set is taken from `turbo-classic`. | ||
| 33 | + | ||
| 34 | +**Inherit from a theme whose ground is the same as yours.** The colours you leave out were chosen against the background of the theme you inherit from, so a dark theme built on `turbo-classic` will show, here and there, a colour picked for Borland navy. If you are writing a dark theme, inherit from `turbo-dark`, `cappuccino`, `catppuccin-frappe`, `cobalt`, `darcula` or `monochrome-dark`; if a light one, from `borland-light`, `catppuccin-latte`, `intellij-light` or `monochrome-light`. That is also why the eleven themes that ship in the binary each state their palette in full rather than inheriting most of it — a test holds them to it, because a shipped theme is one the project is answerable for. | ||
| 35 | + | ||
| 36 | +## 3. Use it | ||
| 37 | + | ||
| 38 | +```bash | ||
| 39 | +turbo-golo -theme mine main.golo | ||
| 40 | +``` | ||
| 41 | + | ||
| 42 | +Or from inside the editor: `Options ▸ Theme…`, which lists every theme it can find. | ||
| 43 | + | ||
| 44 | +## 4. Iterate | ||
| 45 | + | ||
| 46 | +Edit the file, then restart the editor. There is no live reload. | ||
| 47 | + | ||
| 48 | +If the theme fails to load, Turbo Golo falls back to the default rather than refusing to start. To see *why* it failed: | ||
| 49 | + | ||
| 50 | +```bash | ||
| 51 | +turbo-golo -list-themes | ||
| 52 | +``` | ||
| 53 | + | ||
| 54 | +A broken theme is listed with the parse error beside it — an unknown colour name is an error, not a silent fallback, so a typo is pointed at rather than quietly repainting half the screen. | ||
| 55 | + | ||
| 56 | +## 5. Check it stays readable | ||
| 57 | + | ||
| 58 | +turbo-core, the library the themes ship in, holds every one of them to five measured rules, and they are worth applying to your own. Its test suite runs them. | ||
| 59 | + | ||
| 60 | +| Rule | Why | | ||
| 61 | +| --- | --- | | ||
| 62 | +| The cursor is at least 64 apart from the line it sits on, in its strongest channel | A terminal draws its cursor over the cell; one that blends in cannot be found | | ||
| 63 | +| The cursor is never a plain reversal of that line | A terminal that draws its cursor by inverting the cell would invert it back into invisibility | | ||
| 64 | +| The current line is at least 16 from the page | `turbo-dark` once used ten, which is no highlight at all | | ||
| 65 | +| Text you have to read is at least 64 from its background | Furniture — the desktop, a shadow, a scrollbar trough, a disabled entry — is exempt: it exists to recede | | ||
| 66 | +| Comments read at 4.5:1 or better against their background, by WCAG relative luminance | A comment is prose, read word by word. `turbo-classic` drew them in `#808080` on its navy: 128 channel values apart, so the rule above waved it through, and 4.05:1 to read, which is below the W3C's floor for body text. | | ||
| 67 | + | ||
| 68 | +The contrast rule is applied to every shipped theme but the two Catppuccin ones, and the exemption is written where it is made: their colours are somebody else's published palette, faithfully copied, and Catppuccin puts comments at 2.87:1 in Frappé and 2.83:1 in Latte. A theme called Catppuccin that is not those exact values is a different theme wearing a borrowed name, so the fix — if anyone wants one — is upstream. | ||
| 69 | + | ||
| 70 | +It is applied to comments and to nothing else. `syntax.punctuation` is quieter still in several themes and stays that way: punctuation is recognised by shape, not read. | ||
| 71 | + | ||
| 72 | +A sixth rule catches the mistake no measurement finds: **two syntax classes a reader meets side by side must not be drawn identically**. `turbo-classic` once painted `syntax.link` the same lime as `syntax.string`, so a Markdown link and an inline code span were the same thing on screen — every colour readable, every key set, and the two simply equal. Both monochromes pass this rule with no hue at all, by using bold, italic and underline instead. | ||
| 73 | + | ||
| 74 | +## Variants | ||
| 75 | + | ||
| 76 | +**Override a shipped theme rather than adding one.** Name your file after it — `turbo-classic.toml` — and yours wins. The embedded one is not replaced, so deleting your file brings it back. | ||
| 77 | + | ||
| 78 | +**Start from scratch.** Leave `inherits` out. Set at least `default`; every key you do not set falls back along the dots to it, so a theme with one line is still a usable theme. | ||
| 79 | + | ||
| 80 | +**Colour only the syntax.** One key does it: | ||
| 81 | + | ||
| 82 | +```toml | ||
| 83 | +[colors] | ||
| 84 | +syntax = { fg = "silver" } | ||
| 85 | +``` | ||
| 86 | + | ||
| 87 | +`syntax.keyword`, `syntax.string` and the rest all fall back to it. | ||
| 88 | + | ||
| 89 | +**Keep the terminal's own colours.** Use `default` as a colour value: | ||
| 90 | + | ||
| 91 | +```toml | ||
| 92 | +[colors] | ||
| 93 | +"editor.text" = { fg = "default", bg = "default" } | ||
| 94 | +``` | ||
| 95 | + | ||
| 96 | +**Test it in a checkout without installing it.** Point the editor at any directory: | ||
| 97 | + | ||
| 98 | +```bash | ||
| 99 | +TURBO_GOLO_THEME_DIR=./my-themes turbo-golo -theme mine main.golo | ||
| 100 | +``` | ||
| 101 | + | ||
| 102 | +**The cursor is hard to see.** `editor.cursor` does two things: its **background** is sent to the terminal as the cursor's own colour, and it also paints the cell underneath as a fallback for terminals that ignore that. Set it to something loud: | ||
| 103 | + | ||
| 104 | +```toml | ||
| 105 | +[colors] | ||
| 106 | +"editor.cursor" = { fg = "#000000", bg = "#ff8700" } | ||
| 107 | +``` | ||
| 108 | + | ||
| 109 | +Two things make a cursor colour a bad one, and the test suite rejects both: a plain reversal of the line — which terminals that draw their cursor by inverting the cell turn back into invisibility — and anything less than 64 channel values away from the line it sits on. | ||
| 110 | + | ||
| 111 | +**The cursor's line is hard to find.** That is `editor.currentline`, and it is a different key. It has to differ from `editor.text` by at least 16 channel values to count as a highlight at all. | ||
| 112 | + | ||
| 113 | +**Golo types and builtins look alike.** In `turbo-classic` a capitalised name such as `Point` is `syntax.type`, and `println` is `syntax.builtin`; both are aqua on navy, and only the builtin is bold. If that is too close for you, give one of them a colour of its own: | ||
| 114 | + | ||
| 115 | +```toml | ||
| 116 | +[colors] | ||
| 117 | +"syntax.type" = { fg = "yellow" } | ||
| 118 | +"syntax.builtin" = { fg = "aqua", bold = true } | ||
| 119 | +``` | ||
| 120 | + | ||
| 121 | +**Markdown and HTML look plain.** Five keys belong to the markup languages and have no equivalent in Golo, so a theme written before they existed does not set them: | ||
| 122 | + | ||
| 123 | +```toml | ||
| 124 | +[colors] | ||
| 125 | +"syntax.heading" = { fg = "white", bold = true } | ||
| 126 | +"syntax.tag" = { fg = "aqua" } | ||
| 127 | +"syntax.attribute" = { fg = "yellow" } | ||
| 128 | +"syntax.emphasis" = { fg = "fuchsia", bold = true } | ||
| 129 | +"syntax.link" = { fg = "aqua", underline = true } | ||
| 130 | +``` | ||
| 131 | + | ||
| 132 | +Give `syntax.link` a different colour from `syntax.string`: a link and an inline `code` span sit side by side in most prose, and sharing a colour makes them one blur. The shipped `turbo-classic` had exactly that fault until it was looked at on a real terminal. | ||
| 133 | + | ||
| 134 | +**The project tree looks flat.** It has four keys of its own, and none of them falls back to `list`: | ||
| 135 | + | ||
| 136 | +```toml | ||
| 137 | +[colors] | ||
| 138 | +"tree.text" = { fg = "silver", bg = "navy" } | ||
| 139 | +"tree.directory" = { fg = "white", bg = "navy", bold = true } | ||
| 140 | +"tree.selected" = { fg = "black", bg = "aqua" } | ||
| 141 | +"tree.unfocused" = { fg = "black", bg = "gray" } | ||
| 142 | +``` | ||
| 143 | + | ||
| 144 | +Give `tree.text` the same background as `window.body`, so the tree looks like part of its window, and make `tree.selected` clearly different from it — the test suite holds every shipped theme to at least 64 channel values between the two, because a highlight the same colour as the page is no highlight. | ||
| 145 | + | ||
| 146 | +**Terminal windows look wrong.** They have two keys of their own, and neither falls back to `editor`: | ||
| 147 | + | ||
| 148 | +```toml | ||
| 149 | +[colors] | ||
| 150 | +"terminal.text" = { fg = "silver", bg = "black" } | ||
| 151 | +"terminal.cursor" = { fg = "black", bg = "aqua" } | ||
| 152 | +``` | ||
| 153 | + | ||
| 154 | +`terminal.text` is what a shell's output gets when it names no colour of its own — set it to something close to a real terminal rather than to your editor background, or `less` and `htop` will look out of place. A program that does name its colours keeps them either way. | ||
| 155 | + | ||
| 156 | +## See also | ||
| 157 | + | ||
| 158 | +- Every key you may set, and every colour name: [theme file reference](../reference/themes.md) | ||
| 159 | +- Which class each piece of Golo gets: [Languages coloured](../reference/languages.md) | ||
| 160 | +- Why the format is TOML with two kinds of inheritance: [Design decisions](../explanation/design-decisions.md) | ||
added
docs/en/reference/acp.md +239 -0 | new file mode 100644 | ||
| @@ -0,0 +1,239 @@ | ||
| 1 | +# Agents and ACP | |
| 2 | + | |
| 3 | +Turbo Golo is a client for the [Agent Client Protocol](https://agentclientprotocol.com). It starts each agent as a child process and exchanges JSON-RPC 2.0 messages with it over stdin and stdout, one message per line. | |
| 4 | + | |
| 5 | +## Where the file lives | |
| 6 | + | |
| 7 | +| Path | Read | Purpose | | |
| 8 | +| --- | --- | --- | | |
| 9 | +| `~/.config/turbo-golo/acp.toml` | first | Agents you want in every project | | |
| 10 | +| `<project>/.turbo-gololo/acp.toml` | second | Agents belonging to this project | | |
| 11 | + | |
| 12 | +Both are optional. Where an agent's `name` appears in both, the project's replaces the user's, being the more specific statement — the same rule [snippets](snippets.md) follow. A missing file is not an error; a file that is present but unreadable is, and is reported under **Agent ▸ Agent status** rather than silently leaving the menu empty. | |
| 13 | + | |
| 14 | +`TURBO_GOLO_DIR` overrides the directory the user-level file is looked for in. The project file is always `.turbo-gololo/acp.toml` under the directory the editor was started in — there is no walk up the tree, for the same reason [project settings](project-settings.md) do not walk up. | |
| 15 | + | |
| 16 | +## File format | |
| 17 | + | |
| 18 | +One `[[agent]]` block per agent, in the order you want them in the menu. | |
| 19 | + | |
| 20 | +```toml | |
| 21 | +[[agent]] | |
| 22 | +name = "Bob (llama.cpp)" | |
| 23 | +command = "docker" | |
| 24 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | |
| 25 | +env = { TELEMETRY_ENABLED = "false" } | |
| 26 | +cwd = "." | |
| 27 | +``` | |
| 28 | + | |
| 29 | +| Key | Type | Required | Meaning | | |
| 30 | +| --- | --- | --- | --- | | |
| 31 | +| `name` | string | **yes** | What the Agent menu shows and what the window is titled. Must be unique within the merged set. | | |
| 32 | +| `command` | string | **yes** | The executable to run. Looked up on `PATH` unless it contains a separator. | | |
| 33 | +| `args` | list of strings | no | Its arguments, passed as given — no shell, so no quoting, globbing or `&&`. | | |
| 34 | +| `env` | table of strings | no | Environment variables added to the ones the editor was started with. A name given here wins. | | |
| 35 | +| `cwd` | string | no | Where the process starts, and the `cwd` the agent is told about. Relative to the project root. Defaults to the project root. | | |
| 36 | + | |
| 37 | +`env` may also be written as a sub-table, which is the same thing: | |
| 38 | + | |
| 39 | +```toml | |
| 40 | +[[agent]] | |
| 41 | +name = "Bob (llama.cpp)" | |
| 42 | +command = "docker" | |
| 43 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | |
| 44 | + | |
| 45 | +[agent.env] | |
| 46 | +TELEMETRY_ENABLED = "false" | |
| 47 | +``` | |
| 48 | + | |
| 49 | +### What is refused | |
| 50 | + | |
| 51 | +The file is refused as a whole, rather than partly loaded, when any of these hold. A half-loaded menu offering three of your five agents is worse than an error saying why. | |
| 52 | + | |
| 53 | +| Problem | Message | | |
| 54 | +| --- | --- | | |
| 55 | +| an agent with no `name` | `reading …/acp.toml: agent 1 has no name` | | |
| 56 | +| an agent with no `command` | `reading …/acp.toml: agent "Bob" has no command` | | |
| 57 | +| two agents with the same `name` | `reading …/acp.toml: two agents are called "Bob"` | | |
| 58 | +| a key the format does not define | `reading …/acp.toml: agent.comand is not a key this file has` | | |
| 59 | + | |
| 60 | +The last one is deliberate: a misspelt key that was quietly ignored would look exactly like one that had no effect. | |
| 61 | + | |
| 62 | +## The Agent menu | |
| 63 | + | |
| 64 | +`Alt-A` opens it. It is on the bar whether or not any agent is configured, because that is where **Create agents file** has to be reachable from. | |
| 65 | + | |
| 66 | +| Item | Enabled when | Effect | | |
| 67 | +| --- | --- | --- | | |
| 68 | +| *one item per agent, by name* | always | Start that agent and open a window on it | | |
| 69 | +| **Create agents file** | no `acp.toml` in the project | Write the starter file and open it | | |
| 70 | +| **Cancel turn** | a turn is running in the front window | `session/cancel` | | |
| 71 | +| **Agent status** | always | What was loaded, what each command line is, and what failed | | |
| 72 | + | |
| 73 | +## Keys inside an agent window | |
| 74 | + | |
| 75 | +An agent window is an ordinary window: `F6`, `Alt-1`…`Alt-9`, Tile, Maximise, `[x]` and `[■]` all work on it. Inside it: | |
| 76 | + | |
| 77 | +| Key | Effect | | |
| 78 | +| --- | --- | | |
| 79 | +| `Enter` | Send the input box as a prompt | | |
| 80 | +| `Alt-Enter` | Insert a newline in the input box | | |
| 81 | +| `Tab` | Move focus between the conversation and the input box | | |
| 82 | +| `Ctrl-C`, `Ctrl-Ins` | Copy the selection, or the block the cursor is on | | |
| 83 | +| `Esc` | Drop the selection; with none, cancel the turn in progress | | |
| 84 | +| `Ctrl-W` | Close the window and stop the agent | | |
| 85 | + | |
| 86 | +With the **input box** focused: | |
| 87 | + | |
| 88 | +| Key | Effect | | |
| 89 | +| --- | --- | | |
| 90 | +| `↑` `↓` `←` `→` `Home` `End` | Move the cursor in what you are typing | | |
| 91 | +| `Backspace` `Delete` | Edit it; backspace at the start of a line joins it to the one above | | |
| 92 | +| `/` as the first character | Open the list of the agent's commands — see [Commands and mentions](#commands-and-mentions) | | |
| 93 | +| `@` | Open the list of the project's files, narrowed by what you type after it | | |
| 94 | +| `↑` `↓` `PgUp` `PgDn`, list open | Move through the list | | |
| 95 | +| `Tab`, list open | Take the highlighted entry | | |
| 96 | +| `Enter`, list open | Take the highlighted entry; on a word that is already complete, send | | |
| 97 | +| `Esc`, list open | Close the list until the text changes | | |
| 98 | + | |
| 99 | +With the **conversation** focused: | |
| 100 | + | |
| 101 | +| Key | Effect | | |
| 102 | +| --- | --- | | |
| 103 | +| `↑` `↓` | Move the cursor one line | | |
| 104 | +| `PgUp` `PgDn` | Move it a screenful | | |
| 105 | +| `Home` `End` | The start of the conversation, and the end | | |
| 106 | +| `Shift-` any of those | Extend the selection instead | | |
| 107 | +| Drag with button 1 | Select by hand | | |
| 108 | +| Wheel | Scroll three lines, leaving the cursor where it is | | |
| 109 | + | |
| 110 | +Unlike a terminal window, an agent window does **not** take the editor's shortcuts: there is no shell to need `Ctrl-F`, so it keeps its usual meaning. `Ctrl-C` is the exception, and only because nothing else in an agent window wants it. | |
| 111 | + | |
| 112 | +## Commands and mentions | |
| 113 | + | |
| 114 | +Two characters open a list over the bottom of the conversation while you type. They are the same two Zed uses, so an agent's own documentation — "type `/web` to search" — holds here too. | |
| 115 | + | |
| 116 | +### `/` — the agent's commands | |
| 117 | + | |
| 118 | +An agent may announce commands with `available_commands_update`, at the start of the session or at any point during it. Typing `/` as the **first character** of the box lists them: the name, the agent's description, and, in angle brackets, what it expects after the name when it expects something. Keep typing to narrow the list; the match is on the start of the name and ignores case. | |
| 119 | + | |
| 120 | +`Tab` completes the highlighted command. A command that takes input is completed with a trailing space, so the next thing you type is its argument; one that takes none is completed to the bare name. `Enter` completes too, except on a word that already reads exactly as a command, where it sends. | |
| 121 | + | |
| 122 | +On the wire a command is **text**: `/web agent client protocol` goes out as one text block, and the agent recognises it by its first word. That is the whole protocol for commands, and it is why a `/` anywhere but the start of the box is just a character. | |
| 123 | + | |
| 124 | +With no commands announced, `/` is a character and `Tab` keeps its ordinary meaning. **Agent ▸ Agent status** lists the commands with their descriptions. | |
| 125 | + | |
| 126 | +### `@` — a file from the project | |
| 127 | + | |
| 128 | +Typing `@` anywhere in the box lists the project's files, relative to the project root with forward slashes. What you type after the `@` narrows the list: files whose own name begins with it come first, then files whose path merely contains it. `Tab` or `Enter` completes the highlighted one and adds a space. | |
| 129 | + | |
| 130 | +When the prompt is sent, each `@name` that names a file the list knew becomes a content block **in place of the name**: | |
| 131 | + | |
| 132 | +| The agent declared | The block sent | | |
| 133 | +| --- | --- | | |
| 134 | +| `promptCapabilities.embeddedContext: true` | `resource` — the file's `uri`, `mimeType` and full `text`, read the way `fs/read_text_file` reads it: from the open buffer when the file is open and modified | | |
| 135 | +| anything else, or the file could not be read | `resource_link` — the `uri`, `name` and `mimeType`, for the agent to fetch itself | | |
| 136 | + | |
| 137 | +The words either side go as text blocks, so `explain @docs/README.md please` is three blocks: `explain `, the file, ` please`. The conversation keeps the line as you typed it. | |
| 138 | + | |
| 139 | +A word that begins with `@` and names no file stays text — an e-mail address in a prompt is not a file — and `@main.go` does not name `main.gopher`: the name has to end the word. | |
| 140 | + | |
| 141 | +The list is the project walked from its root, `.git` left out, at most 5 000 files, and at most 200 of them shown at once. Past either limit, type one more letter. It is walked afresh each time `@` opens the list, so a file the agent just created is in it. | |
| 142 | + | |
| 143 | +## Copying | |
| 144 | + | |
| 145 | +Selection is by **whole lines**. Nothing in a conversation is edited, so half a line is never what somebody means, and whole lines keep a copied code block's indentation intact. | |
| 146 | + | |
| 147 | +With nothing selected, copying takes the **region the cursor is on**: one fenced code block, one passage of prose, one tool call's output. A speaker's label and a tool call's heading are furniture and are regions of their own, so neither is ever copied with what it sits above. | |
| 148 | + | |
| 149 | +The indentation the conversation is drawn with is removed, so pasted code is flush. | |
| 150 | + | |
| 151 | +The text goes to two places at once: | |
| 152 | + | |
| 153 | +| Clipboard | How | Pasted with | | |
| 154 | +| --- | --- | --- | | |
| 155 | +| The editor's | directly | `Shift-Ins`, into a file open here | | |
| 156 | +| The system's | OSC 52, through the terminal | `Ctrl-V`, anywhere else | | |
| 157 | + | |
| 158 | +Nothing checks whether the terminal accepted the second: there is no reply to check, and a terminal may refuse OSC 52 for security or need it turned on. The editor's own clipboard has the text either way, and the status bar says how many lines were copied. | |
| 159 | + | |
| 160 | +## How much of the protocol is implemented | |
| 161 | + | |
| 162 | +Protocol version **1**. Turbo Golo sends its version in `initialize` and accepts whatever version the agent answers with, provided it is one it knows. | |
| 163 | + | |
| 164 | +### What the editor calls on the agent | |
| 165 | + | |
| 166 | +| Method | Implemented | Notes | | |
| 167 | +| --- | --- | --- | | |
| 168 | +| `initialize` | yes | Advertises the `fs` capability below; `terminal` is not advertised | | |
| 169 | +| `session/new` | yes | `cwd` from the agent's `cwd` key; `mcpServers` is always empty — MCP servers are the agent's own business | | |
| 170 | +| `session/prompt` | yes | Text blocks, and one `resource` or `resource_link` block per file named with `@` — see [Commands and mentions](#commands-and-mentions) | | |
| 171 | +| `session/cancel` | yes | `Esc`, and **Agent ▸ Cancel turn** | | |
| 172 | +| `session/load` | **no** | Conversations do not survive closing the window | | |
| 173 | +| `authenticate` | **no** | An agent that lists `authMethods` is reported as needing a login the editor cannot perform | | |
| 174 | + | |
| 175 | +### What the agent may call on the editor | |
| 176 | + | |
| 177 | +| Method | Implemented | Notes | | |
| 178 | +| --- | --- | --- | | |
| 179 | +| `session/update` | yes | See the table below | | |
| 180 | +| `session/request_permission` | yes | A modal dialog carrying the agent's own options | | |
| 181 | +| `fs/read_text_file` | yes | From the open buffer when the file is open and modified, otherwise from disk | | |
| 182 | +| `fs/write_text_file` | yes | Into the open buffer when the file is open, otherwise to disk | | |
| 183 | +| `terminal/*` | **no** | Not advertised, so a conforming agent will not ask | | |
| 184 | + | |
| 185 | +### Session updates | |
| 186 | + | |
| 187 | +| `sessionUpdate` | Shown as | | |
| 188 | +| --- | --- | | |
| 189 | +| `agent_message_chunk` | The agent's reply, appended as it arrives | | |
| 190 | +| `agent_thought_chunk` | The same, in the comment colour, under a *thinking* label | | |
| 191 | +| `user_message_chunk` | Your own message, as the agent echoes it back | | |
| 192 | +| `tool_call` | A line naming the tool and its title, with its status | | |
| 193 | +| `tool_call_update` | Folded onto the line the `toolCallId` matches, carrying its output | | |
| 194 | +| `plan` | The entries as a list, each with its status | | |
| 195 | +| `available_commands_update` | The list `/` opens in the input box; also listed, with descriptions, by **Agent ▸ Agent status** | | |
| 196 | +| `usage_update` | The token count on the status bar while the window is in front | | |
| 197 | +| anything else | Ignored, and counted; the count is in **Agent status** | | |
| 198 | + | |
| 199 | +While a turn is running, the rule between the panes turns a spinner. It is drawn from the clock rather than from a counter, so two windows thinking at once turn in step and nothing has to be reset when a turn begins. The window's *title* deliberately does not animate: it is also what the window list and the `Alt`-digit menu show, and a name changing eight times a second makes both flicker. | |
| 200 | + | |
| 201 | +An unknown update is ignored rather than refused: the protocol grows, and an editor that stopped talking to an agent because it learnt a new kind of message would be wrong more often than it was right. | |
| 202 | + | |
| 203 | +## Colouring | |
| 204 | + | |
| 205 | +The conversation is drawn with keys every theme already sets, so none of them needed touching: | |
| 206 | + | |
| 207 | +| Part | Class | | |
| 208 | +| --- | --- | | |
| 209 | +| A speaker's name | `syntax.keyword` | | |
| 210 | +| A thought | `syntax.comment` | | |
| 211 | +| A tool call and its status | `syntax.type` | | |
| 212 | +| A failed tool call, and the editor's own notices | `diagnostic.error` | | |
| 213 | +| A selected line, and the cursor's bar | `editor.selection` | | |
| 214 | +| Code inside a fence | the scanner for the fence's language | | |
| 215 | +| Everything else | the window's plain text | | |
| 216 | + | |
| 217 | +A fenced block naming a language the editor colours — `golo`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash` — is coloured by that scanner. One naming anything else, or nothing at all, is left plain. | |
| 218 | + | |
| 219 | +## Tracing the conversation with an agent | |
| 220 | + | |
| 221 | +| Variable | Effect | | |
| 222 | +| --- | --- | | |
| 223 | +| `TURBO_ACP_TRACE=<file>` | Append every message to and from every agent to that file, one per line, stamped with the time and marked `->` (sent) or `<-` (received) | | |
| 224 | + | |
| 225 | +It is for the one question the screen cannot answer — *what did the agent actually send?* An update this editor cannot decode is counted under **Agent ▸ Agent status**, which also names the last one and its error; the trace shows the message itself. A file that cannot be opened means no trace and nothing else: the trace is never allowed to break the editor. | |
| 226 | + | |
| 227 | +## Limits | |
| 228 | + | |
| 229 | +- **One session per window.** Closing the window ends the session; there is no resume. | |
| 230 | +- **Text and files only.** The editor sends text, and the files you name with `@`; not images or audio, whatever the agent's `promptCapabilities` say. | |
| 231 | +- **No authentication.** An agent that requires a login must be logged in by its own CLI before the editor starts it. | |
| 232 | +- **`args` are not a shell command.** `command = "sh"`, `args = ["-c", "…"]` is how to get one deliberately. | |
| 233 | +- **One entry is capped** at a megabyte of text. An agent printing a whole build log cannot make the window unusable; what was dropped is said in the entry itself. | |
| 234 | + | |
| 235 | +## See also | |
| 236 | + | |
| 237 | +- The task: [How to talk to a coding agent from the editor](../how-to/talk-to-an-agent.md) | |
| 238 | +- The reasoning: [Agent windows](../explanation/agent-windows.md) | |
| 239 | +- The protocol: [agentclientprotocol.com](https://agentclientprotocol.com) | |
| new file mode 100644 | |||
| @@ -0,0 +1,239 @@ | |||
| 1 | +# Agents and ACP | ||
| 2 | + | ||
| 3 | +Turbo Golo is a client for the [Agent Client Protocol](https://agentclientprotocol.com). It starts each agent as a child process and exchanges JSON-RPC 2.0 messages with it over stdin and stdout, one message per line. | ||
| 4 | + | ||
| 5 | +## Where the file lives | ||
| 6 | + | ||
| 7 | +| Path | Read | Purpose | | ||
| 8 | +| --- | --- | --- | | ||
| 9 | +| `~/.config/turbo-golo/acp.toml` | first | Agents you want in every project | | ||
| 10 | +| `<project>/.turbo-gololo/acp.toml` | second | Agents belonging to this project | | ||
| 11 | + | ||
| 12 | +Both are optional. Where an agent's `name` appears in both, the project's replaces the user's, being the more specific statement — the same rule [snippets](snippets.md) follow. A missing file is not an error; a file that is present but unreadable is, and is reported under **Agent ▸ Agent status** rather than silently leaving the menu empty. | ||
| 13 | + | ||
| 14 | +`TURBO_GOLO_DIR` overrides the directory the user-level file is looked for in. The project file is always `.turbo-gololo/acp.toml` under the directory the editor was started in — there is no walk up the tree, for the same reason [project settings](project-settings.md) do not walk up. | ||
| 15 | + | ||
| 16 | +## File format | ||
| 17 | + | ||
| 18 | +One `[[agent]]` block per agent, in the order you want them in the menu. | ||
| 19 | + | ||
| 20 | +```toml | ||
| 21 | +[[agent]] | ||
| 22 | +name = "Bob (llama.cpp)" | ||
| 23 | +command = "docker" | ||
| 24 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | ||
| 25 | +env = { TELEMETRY_ENABLED = "false" } | ||
| 26 | +cwd = "." | ||
| 27 | +``` | ||
| 28 | + | ||
| 29 | +| Key | Type | Required | Meaning | | ||
| 30 | +| --- | --- | --- | --- | | ||
| 31 | +| `name` | string | **yes** | What the Agent menu shows and what the window is titled. Must be unique within the merged set. | | ||
| 32 | +| `command` | string | **yes** | The executable to run. Looked up on `PATH` unless it contains a separator. | | ||
| 33 | +| `args` | list of strings | no | Its arguments, passed as given — no shell, so no quoting, globbing or `&&`. | | ||
| 34 | +| `env` | table of strings | no | Environment variables added to the ones the editor was started with. A name given here wins. | | ||
| 35 | +| `cwd` | string | no | Where the process starts, and the `cwd` the agent is told about. Relative to the project root. Defaults to the project root. | | ||
| 36 | + | ||
| 37 | +`env` may also be written as a sub-table, which is the same thing: | ||
| 38 | + | ||
| 39 | +```toml | ||
| 40 | +[[agent]] | ||
| 41 | +name = "Bob (llama.cpp)" | ||
| 42 | +command = "docker" | ||
| 43 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | ||
| 44 | + | ||
| 45 | +[agent.env] | ||
| 46 | +TELEMETRY_ENABLED = "false" | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +### What is refused | ||
| 50 | + | ||
| 51 | +The file is refused as a whole, rather than partly loaded, when any of these hold. A half-loaded menu offering three of your five agents is worse than an error saying why. | ||
| 52 | + | ||
| 53 | +| Problem | Message | | ||
| 54 | +| --- | --- | | ||
| 55 | +| an agent with no `name` | `reading …/acp.toml: agent 1 has no name` | | ||
| 56 | +| an agent with no `command` | `reading …/acp.toml: agent "Bob" has no command` | | ||
| 57 | +| two agents with the same `name` | `reading …/acp.toml: two agents are called "Bob"` | | ||
| 58 | +| a key the format does not define | `reading …/acp.toml: agent.comand is not a key this file has` | | ||
| 59 | + | ||
| 60 | +The last one is deliberate: a misspelt key that was quietly ignored would look exactly like one that had no effect. | ||
| 61 | + | ||
| 62 | +## The Agent menu | ||
| 63 | + | ||
| 64 | +`Alt-A` opens it. It is on the bar whether or not any agent is configured, because that is where **Create agents file** has to be reachable from. | ||
| 65 | + | ||
| 66 | +| Item | Enabled when | Effect | | ||
| 67 | +| --- | --- | --- | | ||
| 68 | +| *one item per agent, by name* | always | Start that agent and open a window on it | | ||
| 69 | +| **Create agents file** | no `acp.toml` in the project | Write the starter file and open it | | ||
| 70 | +| **Cancel turn** | a turn is running in the front window | `session/cancel` | | ||
| 71 | +| **Agent status** | always | What was loaded, what each command line is, and what failed | | ||
| 72 | + | ||
| 73 | +## Keys inside an agent window | ||
| 74 | + | ||
| 75 | +An agent window is an ordinary window: `F6`, `Alt-1`…`Alt-9`, Tile, Maximise, `[x]` and `[■]` all work on it. Inside it: | ||
| 76 | + | ||
| 77 | +| Key | Effect | | ||
| 78 | +| --- | --- | | ||
| 79 | +| `Enter` | Send the input box as a prompt | | ||
| 80 | +| `Alt-Enter` | Insert a newline in the input box | | ||
| 81 | +| `Tab` | Move focus between the conversation and the input box | | ||
| 82 | +| `Ctrl-C`, `Ctrl-Ins` | Copy the selection, or the block the cursor is on | | ||
| 83 | +| `Esc` | Drop the selection; with none, cancel the turn in progress | | ||
| 84 | +| `Ctrl-W` | Close the window and stop the agent | | ||
| 85 | + | ||
| 86 | +With the **input box** focused: | ||
| 87 | + | ||
| 88 | +| Key | Effect | | ||
| 89 | +| --- | --- | | ||
| 90 | +| `↑` `↓` `←` `→` `Home` `End` | Move the cursor in what you are typing | | ||
| 91 | +| `Backspace` `Delete` | Edit it; backspace at the start of a line joins it to the one above | | ||
| 92 | +| `/` as the first character | Open the list of the agent's commands — see [Commands and mentions](#commands-and-mentions) | | ||
| 93 | +| `@` | Open the list of the project's files, narrowed by what you type after it | | ||
| 94 | +| `↑` `↓` `PgUp` `PgDn`, list open | Move through the list | | ||
| 95 | +| `Tab`, list open | Take the highlighted entry | | ||
| 96 | +| `Enter`, list open | Take the highlighted entry; on a word that is already complete, send | | ||
| 97 | +| `Esc`, list open | Close the list until the text changes | | ||
| 98 | + | ||
| 99 | +With the **conversation** focused: | ||
| 100 | + | ||
| 101 | +| Key | Effect | | ||
| 102 | +| --- | --- | | ||
| 103 | +| `↑` `↓` | Move the cursor one line | | ||
| 104 | +| `PgUp` `PgDn` | Move it a screenful | | ||
| 105 | +| `Home` `End` | The start of the conversation, and the end | | ||
| 106 | +| `Shift-` any of those | Extend the selection instead | | ||
| 107 | +| Drag with button 1 | Select by hand | | ||
| 108 | +| Wheel | Scroll three lines, leaving the cursor where it is | | ||
| 109 | + | ||
| 110 | +Unlike a terminal window, an agent window does **not** take the editor's shortcuts: there is no shell to need `Ctrl-F`, so it keeps its usual meaning. `Ctrl-C` is the exception, and only because nothing else in an agent window wants it. | ||
| 111 | + | ||
| 112 | +## Commands and mentions | ||
| 113 | + | ||
| 114 | +Two characters open a list over the bottom of the conversation while you type. They are the same two Zed uses, so an agent's own documentation — "type `/web` to search" — holds here too. | ||
| 115 | + | ||
| 116 | +### `/` — the agent's commands | ||
| 117 | + | ||
| 118 | +An agent may announce commands with `available_commands_update`, at the start of the session or at any point during it. Typing `/` as the **first character** of the box lists them: the name, the agent's description, and, in angle brackets, what it expects after the name when it expects something. Keep typing to narrow the list; the match is on the start of the name and ignores case. | ||
| 119 | + | ||
| 120 | +`Tab` completes the highlighted command. A command that takes input is completed with a trailing space, so the next thing you type is its argument; one that takes none is completed to the bare name. `Enter` completes too, except on a word that already reads exactly as a command, where it sends. | ||
| 121 | + | ||
| 122 | +On the wire a command is **text**: `/web agent client protocol` goes out as one text block, and the agent recognises it by its first word. That is the whole protocol for commands, and it is why a `/` anywhere but the start of the box is just a character. | ||
| 123 | + | ||
| 124 | +With no commands announced, `/` is a character and `Tab` keeps its ordinary meaning. **Agent ▸ Agent status** lists the commands with their descriptions. | ||
| 125 | + | ||
| 126 | +### `@` — a file from the project | ||
| 127 | + | ||
| 128 | +Typing `@` anywhere in the box lists the project's files, relative to the project root with forward slashes. What you type after the `@` narrows the list: files whose own name begins with it come first, then files whose path merely contains it. `Tab` or `Enter` completes the highlighted one and adds a space. | ||
| 129 | + | ||
| 130 | +When the prompt is sent, each `@name` that names a file the list knew becomes a content block **in place of the name**: | ||
| 131 | + | ||
| 132 | +| The agent declared | The block sent | | ||
| 133 | +| --- | --- | | ||
| 134 | +| `promptCapabilities.embeddedContext: true` | `resource` — the file's `uri`, `mimeType` and full `text`, read the way `fs/read_text_file` reads it: from the open buffer when the file is open and modified | | ||
| 135 | +| anything else, or the file could not be read | `resource_link` — the `uri`, `name` and `mimeType`, for the agent to fetch itself | | ||
| 136 | + | ||
| 137 | +The words either side go as text blocks, so `explain @docs/README.md please` is three blocks: `explain `, the file, ` please`. The conversation keeps the line as you typed it. | ||
| 138 | + | ||
| 139 | +A word that begins with `@` and names no file stays text — an e-mail address in a prompt is not a file — and `@main.go` does not name `main.gopher`: the name has to end the word. | ||
| 140 | + | ||
| 141 | +The list is the project walked from its root, `.git` left out, at most 5 000 files, and at most 200 of them shown at once. Past either limit, type one more letter. It is walked afresh each time `@` opens the list, so a file the agent just created is in it. | ||
| 142 | + | ||
| 143 | +## Copying | ||
| 144 | + | ||
| 145 | +Selection is by **whole lines**. Nothing in a conversation is edited, so half a line is never what somebody means, and whole lines keep a copied code block's indentation intact. | ||
| 146 | + | ||
| 147 | +With nothing selected, copying takes the **region the cursor is on**: one fenced code block, one passage of prose, one tool call's output. A speaker's label and a tool call's heading are furniture and are regions of their own, so neither is ever copied with what it sits above. | ||
| 148 | + | ||
| 149 | +The indentation the conversation is drawn with is removed, so pasted code is flush. | ||
| 150 | + | ||
| 151 | +The text goes to two places at once: | ||
| 152 | + | ||
| 153 | +| Clipboard | How | Pasted with | | ||
| 154 | +| --- | --- | --- | | ||
| 155 | +| The editor's | directly | `Shift-Ins`, into a file open here | | ||
| 156 | +| The system's | OSC 52, through the terminal | `Ctrl-V`, anywhere else | | ||
| 157 | + | ||
| 158 | +Nothing checks whether the terminal accepted the second: there is no reply to check, and a terminal may refuse OSC 52 for security or need it turned on. The editor's own clipboard has the text either way, and the status bar says how many lines were copied. | ||
| 159 | + | ||
| 160 | +## How much of the protocol is implemented | ||
| 161 | + | ||
| 162 | +Protocol version **1**. Turbo Golo sends its version in `initialize` and accepts whatever version the agent answers with, provided it is one it knows. | ||
| 163 | + | ||
| 164 | +### What the editor calls on the agent | ||
| 165 | + | ||
| 166 | +| Method | Implemented | Notes | | ||
| 167 | +| --- | --- | --- | | ||
| 168 | +| `initialize` | yes | Advertises the `fs` capability below; `terminal` is not advertised | | ||
| 169 | +| `session/new` | yes | `cwd` from the agent's `cwd` key; `mcpServers` is always empty — MCP servers are the agent's own business | | ||
| 170 | +| `session/prompt` | yes | Text blocks, and one `resource` or `resource_link` block per file named with `@` — see [Commands and mentions](#commands-and-mentions) | | ||
| 171 | +| `session/cancel` | yes | `Esc`, and **Agent ▸ Cancel turn** | | ||
| 172 | +| `session/load` | **no** | Conversations do not survive closing the window | | ||
| 173 | +| `authenticate` | **no** | An agent that lists `authMethods` is reported as needing a login the editor cannot perform | | ||
| 174 | + | ||
| 175 | +### What the agent may call on the editor | ||
| 176 | + | ||
| 177 | +| Method | Implemented | Notes | | ||
| 178 | +| --- | --- | --- | | ||
| 179 | +| `session/update` | yes | See the table below | | ||
| 180 | +| `session/request_permission` | yes | A modal dialog carrying the agent's own options | | ||
| 181 | +| `fs/read_text_file` | yes | From the open buffer when the file is open and modified, otherwise from disk | | ||
| 182 | +| `fs/write_text_file` | yes | Into the open buffer when the file is open, otherwise to disk | | ||
| 183 | +| `terminal/*` | **no** | Not advertised, so a conforming agent will not ask | | ||
| 184 | + | ||
| 185 | +### Session updates | ||
| 186 | + | ||
| 187 | +| `sessionUpdate` | Shown as | | ||
| 188 | +| --- | --- | | ||
| 189 | +| `agent_message_chunk` | The agent's reply, appended as it arrives | | ||
| 190 | +| `agent_thought_chunk` | The same, in the comment colour, under a *thinking* label | | ||
| 191 | +| `user_message_chunk` | Your own message, as the agent echoes it back | | ||
| 192 | +| `tool_call` | A line naming the tool and its title, with its status | | ||
| 193 | +| `tool_call_update` | Folded onto the line the `toolCallId` matches, carrying its output | | ||
| 194 | +| `plan` | The entries as a list, each with its status | | ||
| 195 | +| `available_commands_update` | The list `/` opens in the input box; also listed, with descriptions, by **Agent ▸ Agent status** | | ||
| 196 | +| `usage_update` | The token count on the status bar while the window is in front | | ||
| 197 | +| anything else | Ignored, and counted; the count is in **Agent status** | | ||
| 198 | + | ||
| 199 | +While a turn is running, the rule between the panes turns a spinner. It is drawn from the clock rather than from a counter, so two windows thinking at once turn in step and nothing has to be reset when a turn begins. The window's *title* deliberately does not animate: it is also what the window list and the `Alt`-digit menu show, and a name changing eight times a second makes both flicker. | ||
| 200 | + | ||
| 201 | +An unknown update is ignored rather than refused: the protocol grows, and an editor that stopped talking to an agent because it learnt a new kind of message would be wrong more often than it was right. | ||
| 202 | + | ||
| 203 | +## Colouring | ||
| 204 | + | ||
| 205 | +The conversation is drawn with keys every theme already sets, so none of them needed touching: | ||
| 206 | + | ||
| 207 | +| Part | Class | | ||
| 208 | +| --- | --- | | ||
| 209 | +| A speaker's name | `syntax.keyword` | | ||
| 210 | +| A thought | `syntax.comment` | | ||
| 211 | +| A tool call and its status | `syntax.type` | | ||
| 212 | +| A failed tool call, and the editor's own notices | `diagnostic.error` | | ||
| 213 | +| A selected line, and the cursor's bar | `editor.selection` | | ||
| 214 | +| Code inside a fence | the scanner for the fence's language | | ||
| 215 | +| Everything else | the window's plain text | | ||
| 216 | + | ||
| 217 | +A fenced block naming a language the editor colours — `golo`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash` — is coloured by that scanner. One naming anything else, or nothing at all, is left plain. | ||
| 218 | + | ||
| 219 | +## Tracing the conversation with an agent | ||
| 220 | + | ||
| 221 | +| Variable | Effect | | ||
| 222 | +| --- | --- | | ||
| 223 | +| `TURBO_ACP_TRACE=<file>` | Append every message to and from every agent to that file, one per line, stamped with the time and marked `->` (sent) or `<-` (received) | | ||
| 224 | + | ||
| 225 | +It is for the one question the screen cannot answer — *what did the agent actually send?* An update this editor cannot decode is counted under **Agent ▸ Agent status**, which also names the last one and its error; the trace shows the message itself. A file that cannot be opened means no trace and nothing else: the trace is never allowed to break the editor. | ||
| 226 | + | ||
| 227 | +## Limits | ||
| 228 | + | ||
| 229 | +- **One session per window.** Closing the window ends the session; there is no resume. | ||
| 230 | +- **Text and files only.** The editor sends text, and the files you name with `@`; not images or audio, whatever the agent's `promptCapabilities` say. | ||
| 231 | +- **No authentication.** An agent that requires a login must be logged in by its own CLI before the editor starts it. | ||
| 232 | +- **`args` are not a shell command.** `command = "sh"`, `args = ["-c", "…"]` is how to get one deliberately. | ||
| 233 | +- **One entry is capped** at a megabyte of text. An agent printing a whole build log cannot make the window unusable; what was dropped is said in the entry itself. | ||
| 234 | + | ||
| 235 | +## See also | ||
| 236 | + | ||
| 237 | +- The task: [How to talk to a coding agent from the editor](../how-to/talk-to-an-agent.md) | ||
| 238 | +- The reasoning: [Agent windows](../explanation/agent-windows.md) | ||
| 239 | +- The protocol: [agentclientprotocol.com](https://agentclientprotocol.com) | ||
added
docs/en/reference/cli.md +109 -0 | new file mode 100644 | ||
| @@ -0,0 +1,109 @@ | ||
| 1 | +# Reference: command line | |
| 2 | + | |
| 3 | +> Neutral description of the `turbo-golo` command, its flags, and the environment it reads. | |
| 4 | + | |
| 5 | +## Synopsis | |
| 6 | + | |
| 7 | +``` | |
| 8 | +turbo-golo [flags] [file...] | |
| 9 | +``` | |
| 10 | + | |
| 11 | +Each `file` is opened in its own window. A file that does not exist yet is opened as an empty buffer bound to that path. With no file at all, one empty untitled window is opened. | |
| 12 | + | |
| 13 | +## Flags | |
| 14 | + | |
| 15 | +| Flag | Type | Default | Description | | |
| 16 | +| --- | --- | --- | --- | | |
| 17 | +| `-theme <name>` | string | the project's, else `turbo-classic` | Theme to start with, overriding the project's own. An unknown name falls back to the default without an error. | | |
| 18 | +| `-list-themes` | bool | `false` | Print every loadable theme with its description, then the user theme directory, and exit. | | |
| 19 | +| `-no-lsp` | bool | `false` | Do not start a language server. Colouring and editing are unaffected. | | |
| 20 | +| `-version` | bool | `false` | Print `Turbo Golo <version>` on one line, with the commit and build date when the build recorded them, and exit. See [the version number](versioning.md). | | |
| 21 | +| `-h`, `-help` | bool | `false` | Print the flag list and exit. | | |
| 22 | + | |
| 23 | +## Environment | |
| 24 | + | |
| 25 | +| Variable | Read by | Effect | | |
| 26 | +| --- | --- | --- | | |
| 27 | +| `TURBO_GOLO_DIR` | user files | Directory to keep the user's own files in — themes and snippets — instead of `~/.config/turbo-golo`. | | |
| 28 | +| `TURBO_GOLO_THEME_DIR` | theme loading | Directory to read user themes from, instead of `<user dir>/themes`. | | |
| 29 | +| `TURBO_GOLO_SNIPPET_DIR` | snippets | Directory to read the user's `snippets.toml` from, instead of `<user dir>`. | | |
| 30 | +| `TERM` | tcell | Which terminal description to use. | | |
| 31 | +| `PATH` | `golo` lookup | Searched first for `golo`; `/usr/local/bin` is tried after it. No other variable names a directory to look in. | | |
| 32 | + | |
| 33 | +## Files | |
| 34 | + | |
| 35 | +| Path | Purpose | | |
| 36 | +| --- | --- | | |
| 37 | +| `$TURBO_GOLO_THEME_DIR/*.toml` | User themes, when the variable is set. | | |
| 38 | +| `./.turbo-golo/settings.toml` | This project's settings, read at start-up and again when saved from the editor. See [project settings](project-settings.md). | | |
| 39 | +| `./.turbo-golo/snippets.toml` | This project's snippets, read each time the Snippets menu opens. See [snippets](snippets.md). | | |
| 40 | +| `./.turbo-golo/tools.toml` | This project's tools, read each time the Golo menu opens. See [Golo tools](golo-tools.md). | | |
| 41 | +| `~/.config/turbo-golo/themes/*.toml` | User themes on Linux (`os.UserConfigDir`). | | |
| 42 | +| `~/.config/turbo-golo/snippets.toml` | User snippets on Linux. | | |
| 43 | +| `~/Library/Application Support/turbo-golo/…` | The same two on macOS. | | |
| 44 | + | |
| 45 | +There is no project marker file. The language server is started in the directory of the first file named on the command line, or in the working directory when none is; no parent directory is looked at. | |
| 46 | + | |
| 47 | +## Exit status | |
| 48 | + | |
| 49 | +| Status | Meaning | | |
| 50 | +| --- | --- | | |
| 51 | +| `0` | The editor exited normally, or an informational flag was used. | | |
| 52 | +| `1` | The terminal could not be opened or initialised. The reason is printed to standard error. | | |
| 53 | + | |
| 54 | +## Make targets | |
| 55 | + | |
| 56 | +Run from a checkout. | |
| 57 | + | |
| 58 | +| Target | What it runs | | |
| 59 | +| --- | --- | | |
| 60 | +| `make help` | List the targets. This is the default. | | |
| 61 | +| `make test` | `go test ./...` | | |
| 62 | +| `make test-verbose` | `go test -v ./...` | | |
| 63 | +| `make cover` | `go test -cover ./...` | | |
| 64 | +| `make build` | `go build -ldflags "…" -o bin/turbo-golo .`, stamping the version, then `scripts/check-version.sh` on the result | | |
| 65 | +| `make version` | Print the version this checkout would stamp, without building | | |
| 66 | +| `make ldflags` | Print the linker flags a stamped build uses | | |
| 67 | +| `make install` | `scripts/install.sh` — build and install onto your PATH | | |
| 68 | +| `make uninstall` | `scripts/install.sh --uninstall` | | |
| 69 | +| `make run FILE=x.golo` | `make build`, then `./bin/turbo-golo x.golo` | | |
| 70 | +| `make fmt` | `go fmt ./...` | | |
| 71 | +| `make vet` | `go vet ./...` | | |
| 72 | +| `make check` | `fmt`, then `vet`, then `test` | | |
| 73 | +| `make clean` | Remove `bin/` | | |
| 74 | + | |
| 75 | +## Examples | |
| 76 | + | |
| 77 | +```bash | |
| 78 | +turbo-golo # one empty window | |
| 79 | +turbo-golo main.golo main_test.golo # two windows | |
| 80 | +turbo-golo -theme turbo-dark main.golo # a different theme | |
| 81 | +turbo-golo -no-lsp main.golo # no language server | |
| 82 | +turbo-golo -list-themes # what themes exist | |
| 83 | +``` | |
| 84 | + | |
| 85 | +## Installer | |
| 86 | + | |
| 87 | +`scripts/install.sh`, also reachable as `make install`. | |
| 88 | + | |
| 89 | +| Option | Description | | |
| 90 | +| --- | --- | | |
| 91 | +| `-p`, `--prefix DIR` | Install into `DIR` instead of `$GOBIN` or `$GOPATH/bin`. | | |
| 92 | +| `--with-server` | Also build and install GoloScript — `golo`, `gogolo` and `wagolo` — from source into `/usr/local/bin`, if `golo` is not already there. Needs `git` and Go. | | |
| 93 | +| `--uninstall` | Remove an installed `turbo-golo` and stop. | | |
| 94 | +| `-h`, `--help` | Print the options and stop. | | |
| 95 | + | |
| 96 | +| Exit status | Meaning | | |
| 97 | +| --- | --- | | |
| 98 | +| `0` | Installed, removed, or help printed. | | |
| 99 | +| `1` | Go missing or too old, the build failed, the built binary did not report its version, or the destination could not be written. Nothing is installed and an existing installation is left untouched. | | |
| 100 | + | |
| 101 | +## Errors | |
| 102 | + | |
| 103 | +| Message | Cause | | |
| 104 | +| --- | --- | | |
| 105 | +| `turbo-golo: opening the terminal: …` | tcell could not open the terminal; usually `TERM` is unset or unknown. | | |
| 106 | +| `turbo-golo: initialising the terminal: …` | The terminal was opened but could not be put into raw mode. | | |
| 107 | +| `turbo-golo: reading …/settings.toml: …` | The project's settings file is not valid; the editor opens with its defaults. | | |
| 108 | +| `Cannot open` (in a dialog) | The path is a directory, or is not readable. | | |
| 109 | +| `Cannot save` (in a dialog) | The directory does not exist, or is not writable. | | |
| new file mode 100644 | |||
| @@ -0,0 +1,109 @@ | |||
| 1 | +# Reference: command line | ||
| 2 | + | ||
| 3 | +> Neutral description of the `turbo-golo` command, its flags, and the environment it reads. | ||
| 4 | + | ||
| 5 | +## Synopsis | ||
| 6 | + | ||
| 7 | +``` | ||
| 8 | +turbo-golo [flags] [file...] | ||
| 9 | +``` | ||
| 10 | + | ||
| 11 | +Each `file` is opened in its own window. A file that does not exist yet is opened as an empty buffer bound to that path. With no file at all, one empty untitled window is opened. | ||
| 12 | + | ||
| 13 | +## Flags | ||
| 14 | + | ||
| 15 | +| Flag | Type | Default | Description | | ||
| 16 | +| --- | --- | --- | --- | | ||
| 17 | +| `-theme <name>` | string | the project's, else `turbo-classic` | Theme to start with, overriding the project's own. An unknown name falls back to the default without an error. | | ||
| 18 | +| `-list-themes` | bool | `false` | Print every loadable theme with its description, then the user theme directory, and exit. | | ||
| 19 | +| `-no-lsp` | bool | `false` | Do not start a language server. Colouring and editing are unaffected. | | ||
| 20 | +| `-version` | bool | `false` | Print `Turbo Golo <version>` on one line, with the commit and build date when the build recorded them, and exit. See [the version number](versioning.md). | | ||
| 21 | +| `-h`, `-help` | bool | `false` | Print the flag list and exit. | | ||
| 22 | + | ||
| 23 | +## Environment | ||
| 24 | + | ||
| 25 | +| Variable | Read by | Effect | | ||
| 26 | +| --- | --- | --- | | ||
| 27 | +| `TURBO_GOLO_DIR` | user files | Directory to keep the user's own files in — themes and snippets — instead of `~/.config/turbo-golo`. | | ||
| 28 | +| `TURBO_GOLO_THEME_DIR` | theme loading | Directory to read user themes from, instead of `<user dir>/themes`. | | ||
| 29 | +| `TURBO_GOLO_SNIPPET_DIR` | snippets | Directory to read the user's `snippets.toml` from, instead of `<user dir>`. | | ||
| 30 | +| `TERM` | tcell | Which terminal description to use. | | ||
| 31 | +| `PATH` | `golo` lookup | Searched first for `golo`; `/usr/local/bin` is tried after it. No other variable names a directory to look in. | | ||
| 32 | + | ||
| 33 | +## Files | ||
| 34 | + | ||
| 35 | +| Path | Purpose | | ||
| 36 | +| --- | --- | | ||
| 37 | +| `$TURBO_GOLO_THEME_DIR/*.toml` | User themes, when the variable is set. | | ||
| 38 | +| `./.turbo-golo/settings.toml` | This project's settings, read at start-up and again when saved from the editor. See [project settings](project-settings.md). | | ||
| 39 | +| `./.turbo-golo/snippets.toml` | This project's snippets, read each time the Snippets menu opens. See [snippets](snippets.md). | | ||
| 40 | +| `./.turbo-golo/tools.toml` | This project's tools, read each time the Golo menu opens. See [Golo tools](golo-tools.md). | | ||
| 41 | +| `~/.config/turbo-golo/themes/*.toml` | User themes on Linux (`os.UserConfigDir`). | | ||
| 42 | +| `~/.config/turbo-golo/snippets.toml` | User snippets on Linux. | | ||
| 43 | +| `~/Library/Application Support/turbo-golo/…` | The same two on macOS. | | ||
| 44 | + | ||
| 45 | +There is no project marker file. The language server is started in the directory of the first file named on the command line, or in the working directory when none is; no parent directory is looked at. | ||
| 46 | + | ||
| 47 | +## Exit status | ||
| 48 | + | ||
| 49 | +| Status | Meaning | | ||
| 50 | +| --- | --- | | ||
| 51 | +| `0` | The editor exited normally, or an informational flag was used. | | ||
| 52 | +| `1` | The terminal could not be opened or initialised. The reason is printed to standard error. | | ||
| 53 | + | ||
| 54 | +## Make targets | ||
| 55 | + | ||
| 56 | +Run from a checkout. | ||
| 57 | + | ||
| 58 | +| Target | What it runs | | ||
| 59 | +| --- | --- | | ||
| 60 | +| `make help` | List the targets. This is the default. | | ||
| 61 | +| `make test` | `go test ./...` | | ||
| 62 | +| `make test-verbose` | `go test -v ./...` | | ||
| 63 | +| `make cover` | `go test -cover ./...` | | ||
| 64 | +| `make build` | `go build -ldflags "…" -o bin/turbo-golo .`, stamping the version, then `scripts/check-version.sh` on the result | | ||
| 65 | +| `make version` | Print the version this checkout would stamp, without building | | ||
| 66 | +| `make ldflags` | Print the linker flags a stamped build uses | | ||
| 67 | +| `make install` | `scripts/install.sh` — build and install onto your PATH | | ||
| 68 | +| `make uninstall` | `scripts/install.sh --uninstall` | | ||
| 69 | +| `make run FILE=x.golo` | `make build`, then `./bin/turbo-golo x.golo` | | ||
| 70 | +| `make fmt` | `go fmt ./...` | | ||
| 71 | +| `make vet` | `go vet ./...` | | ||
| 72 | +| `make check` | `fmt`, then `vet`, then `test` | | ||
| 73 | +| `make clean` | Remove `bin/` | | ||
| 74 | + | ||
| 75 | +## Examples | ||
| 76 | + | ||
| 77 | +```bash | ||
| 78 | +turbo-golo # one empty window | ||
| 79 | +turbo-golo main.golo main_test.golo # two windows | ||
| 80 | +turbo-golo -theme turbo-dark main.golo # a different theme | ||
| 81 | +turbo-golo -no-lsp main.golo # no language server | ||
| 82 | +turbo-golo -list-themes # what themes exist | ||
| 83 | +``` | ||
| 84 | + | ||
| 85 | +## Installer | ||
| 86 | + | ||
| 87 | +`scripts/install.sh`, also reachable as `make install`. | ||
| 88 | + | ||
| 89 | +| Option | Description | | ||
| 90 | +| --- | --- | | ||
| 91 | +| `-p`, `--prefix DIR` | Install into `DIR` instead of `$GOBIN` or `$GOPATH/bin`. | | ||
| 92 | +| `--with-server` | Also build and install GoloScript — `golo`, `gogolo` and `wagolo` — from source into `/usr/local/bin`, if `golo` is not already there. Needs `git` and Go. | | ||
| 93 | +| `--uninstall` | Remove an installed `turbo-golo` and stop. | | ||
| 94 | +| `-h`, `--help` | Print the options and stop. | | ||
| 95 | + | ||
| 96 | +| Exit status | Meaning | | ||
| 97 | +| --- | --- | | ||
| 98 | +| `0` | Installed, removed, or help printed. | | ||
| 99 | +| `1` | Go missing or too old, the build failed, the built binary did not report its version, or the destination could not be written. Nothing is installed and an existing installation is left untouched. | | ||
| 100 | + | ||
| 101 | +## Errors | ||
| 102 | + | ||
| 103 | +| Message | Cause | | ||
| 104 | +| --- | --- | | ||
| 105 | +| `turbo-golo: opening the terminal: …` | tcell could not open the terminal; usually `TERM` is unset or unknown. | | ||
| 106 | +| `turbo-golo: initialising the terminal: …` | The terminal was opened but could not be put into raw mode. | | ||
| 107 | +| `turbo-golo: reading …/settings.toml: …` | The project's settings file is not valid; the editor opens with its defaults. | | ||
| 108 | +| `Cannot open` (in a dialog) | The path is a directory, or is not readable. | | ||
| 109 | +| `Cannot save` (in a dialog) | The directory does not exist, or is not writable. | | ||
added
docs/en/reference/golo-tools.md +244 -0 | new file mode 100644 | ||
| @@ -0,0 +1,244 @@ | ||
| 1 | +# Reference: Golo tools | |
| 2 | + | |
| 3 | +> Neutral description of `.turbo-golo/tools.toml`, the Golo menu, and what running a command does. | |
| 4 | + | |
| 5 | +## File | |
| 6 | + | |
| 7 | +| Property | Value | | |
| 8 | +| --- | --- | | |
| 9 | +| Path | `./.turbo-golo/tools.toml` | | |
| 10 | +| Search | The working directory only. Parent directories are **not** searched. | | |
| 11 | +| Read | Every time one of its menus opens, for the items | | |
| 12 | +| Re-read | Whenever the file's size or modification time changes, for the **set** of menus | | |
| 13 | +| Missing file | Not an error | | |
| 14 | +| Unreadable file | An error, shown in the menu | | |
| 15 | +| User-level file | **None.** Unlike snippets, there is no `~/.config/turbo-golo/tools.toml`. | | |
| 16 | + | |
| 17 | +## File format | |
| 18 | + | |
| 19 | +One `[[tool]]` table per command. | |
| 20 | + | |
| 21 | +| Key | Type | Required | Description | | |
| 22 | +| --- | --- | --- | --- | | |
| 23 | +| `name` | string | yes | What the menu shows. May carry a hot key written with tildes, as in `"~T~est"`. | | |
| 24 | +| `command` | string | yes | The shell command to run | | |
| 25 | +| `output` | string | no | Where its output goes: `popup`, `terminal` or `editor`. Absent means `popup`. | | |
| 26 | +| `menu` | string | no | Which menu it appears in. Absent means `Golo`. Any name; the menu is created for you. May carry a hot key written with tildes. | | |
| 27 | + | |
| 28 | +`menu` is not checked against a list, because there is no list: a name that no other tool uses simply creates a menu. A tool with no `name`, no `command`, or an `output` naming something that does not exist makes the whole file an error. An unknown `output` is **refused rather than corrected**: `"termnial"` would otherwise look as though it had worked while sending the output somewhere else. | |
| 29 | + | |
| 30 | +### Example | |
| 31 | + | |
| 32 | +```toml | |
| 33 | +[[tool]] | |
| 34 | +name = "~T~est" | |
| 35 | +command = "golo --test" | |
| 36 | +output = "popup" | |
| 37 | + | |
| 38 | +[[tool]] | |
| 39 | +name = "~E~cho" | |
| 40 | +command = "echo TADA" | |
| 41 | +output = "terminal" | |
| 42 | +menu = "Tools" | |
| 43 | +``` | |
| 44 | + | |
| 45 | +## The starter file | |
| 46 | + | |
| 47 | +**Golo ▸ Create tools file** writes these nine, in this order: | |
| 48 | + | |
| 49 | +| Name | Command | Output | Menu | | |
| 50 | +| --- | --- | --- | --- | | |
| 51 | +| `~R~un` | `golo {{script, e.g. main.golo}}` | `terminal` | Golo | | |
| 52 | +| `~T~est` | `golo --test` | `popup` | Golo | | |
| 53 | +| `Test ~o~ne` | `golo --test {{test file or directory}}` | `popup` | Golo | | |
| 54 | +| `~D~ebug` | `golo --debug {{script, e.g. main.golo}}` | `terminal` | Golo | | |
| 55 | +| `R~E~PL` | `golo` | `terminal` | Golo | | |
| 56 | +| `~N~ew script` | `golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}` | `popup` | Golo | | |
| 57 | +| `~B~uild native` | `gogolo build -o {{output executable}} {{script, e.g. main.golo}}` | `popup` | Golo | | |
| 58 | +| `Build ~w~asm` | `wagolo build -target={{target: wasi, js or wasip2}} -o {{output .wasm}} {{script, e.g. main.golo}}` | `popup` | Golo | | |
| 59 | +| `~E~cho` | `echo 🎉 tada!` | `terminal` | Tools | | |
| 60 | + | |
| 61 | +`Run` comes first because Golo is a scripting language and running the file is what a Golo programmer does most. Six of them ask for a value before they run — Golo has no manifest, so every command that touches a file has to be told which one — and one names a `menu` of its own. Those two features are invisible unless the starter file shows them. | |
| 62 | + | |
| 63 | +`Run`, `Debug` and `REPL` get a terminal: the first two may read the keyboard, and the third is nothing else. `Build native` needs the Go toolchain on `PATH`; `Build wasm` needs TinyGo, and `wasm-tools` for the `wasip2` target. | |
| 64 | + | |
| 65 | +Every tool names its `output`, including the ones that name the default: the key is the interesting part of the format, and a file where it appears once is a file where nobody notices it exists. | |
| 66 | + | |
| 67 | +The item is greyed out once the project has a tools file, so it cannot overwrite one. The file is written through a temporary file in the same directory, renamed into place. | |
| 68 | + | |
| 69 | +## The Golo menu | |
| 70 | + | |
| 71 | +Always on the bar, whether or not a tools file exists. Its hot key is `Alt-G`. | |
| 72 | + | |
| 73 | +| Item | Condition | | |
| 74 | +| --- | --- | | |
| 75 | +| One line per tool with no `menu`, in file order | The file holds at least one | | |
| 76 | +| `Cannot read tools`, greyed out | The file is present but unreadable | | |
| 77 | +| `Create tools file` | The project has no tools file | | |
| 78 | +| `Open tools file` | The project has one | | |
| 79 | + | |
| 80 | +## Menus a tool asks for | |
| 81 | + | |
| 82 | +A `menu` naming anything other than `Golo` puts a menu of that name on the bar. | |
| 83 | + | |
| 84 | +| Property | Value | | |
| 85 | +| --- | --- | | |
| 86 | +| Position | Between Golo and Help | | |
| 87 | +| Order | The order each name first appears in the file | | |
| 88 | +| Items | One line per tool naming that menu, in file order. Nothing else — `Create tools file` and `Open tools file` stay in Golo. | | |
| 89 | +| Unreadable file | No menus at all; the Golo menu carries the error | | |
| 90 | +| While the editor runs | Added, removed and renamed as the file changes, without restarting | | |
| 91 | + | |
| 92 | +### Hot keys | |
| 93 | + | |
| 94 | +Assigned automatically, because a name from a file cannot be checked against the fixed menus in advance. | |
| 95 | + | |
| 96 | +| Case | Result | | |
| 97 | +| --- | --- | | |
| 98 | +| No tildes in the name | The first letter no other menu has claimed is marked. `Format` becomes `For~m~at`: `F` is File's, `o` is Options', `r` is Run's. | | |
| 99 | +| Tildes naming a free letter | Kept as written. `Doc~k~er` answers to `Alt-K`. | | |
| 100 | +| Tildes naming a taken letter | Dropped, and a free letter chosen instead. `~F~oo` becomes `F~o~o`. | | |
| 101 | +| Every letter taken | No hot key. `F10` and the mouse still open it. | | |
| 102 | + | |
| 103 | +The letters the editor's own menus hold are `F`, `E`, `S`, `R`, `C`, `O`, `W`, `N` (Snippets), `G` (Golo) and `H`. | |
| 104 | + | |
| 105 | +## Running a command | |
| 106 | + | |
| 107 | +Common to every output: | |
| 108 | + | |
| 109 | +| Property | Value | | |
| 110 | +| --- | --- | | |
| 111 | +| Shell | `/bin/sh -c "<command>"` on Linux and macOS; `cmd.exe /S /C "<command>"` — the shell `%COMSPEC%` names — on Windows | | |
| 112 | +| Directory | The directory the editor was started in | | |
| 113 | +| Standard error | Merged into standard output, in the order the command wrote them | | |
| 114 | + | |
| 115 | +Going through a shell means pipes, globs, `&&` and `;` all work, so one tool can be a sequence. On Windows the shell is cmd.exe, which knows `&&`, `|` and `>` but does not expand globs, and where `;` is not a separator. | |
| 116 | + | |
| 117 | +### `output = "popup"` | |
| 118 | + | |
| 119 | +| Property | Value | | |
| 120 | +| --- | --- | | |
| 121 | +| Opens | Immediately, before the command has finished | | |
| 122 | +| Modal | Yes: nothing else in the editor can be used while it is up | | |
| 123 | +| Fills in | As output arrives, following it until you scroll back | | |
| 124 | +| Title while running | `<command> — running` | | |
| 125 | +| Title when finished | `<command> — ok`, or `<command> — exit <n>` | | |
| 126 | +| Empty output, finished | Shows `(no output)` | | |
| 127 | +| Empty output, running | Shows nothing | | |
| 128 | +| Output cap | 10000 lines; past it the oldest go and a `… n earlier lines dropped …` line says so | | |
| 129 | + | |
| 130 | +| Key | Effect | | |
| 131 | +| --- | --- | | |
| 132 | +| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Read through the output | | |
| 133 | +| Wheel | The same | | |
| 134 | +| `Escape`, `Enter`, **Close** | Close it, **stopping the command** if it is still running | | |
| 135 | + | |
| 136 | +Closing stops the command because there is no other way to interrupt one whose output is not in a terminal. | |
| 137 | + | |
| 138 | +### `output = "terminal"` | |
| 139 | + | |
| 140 | +| Property | Value | | |
| 141 | +| --- | --- | | |
| 142 | +| Window | A terminal window of its own, titled with the command | | |
| 143 | +| Environment | The editor's own, with `TERM` set to `xterm-256color` | | |
| 144 | +| After it exits | The window stays, showing its output | | |
| 145 | +| Modal | No: the editor carries on beside it | | |
| 146 | + | |
| 147 | +Because it is a real terminal, colours, paging, `Ctrl-C` and reading from the keyboard all work — `golo --test`'s green ticks, `readln` in a script, the REPL's prompt. See [Terminal windows](terminal.md). | |
| 148 | + | |
| 149 | +Keys in a **finished** terminal window: | |
| 150 | + | |
| 151 | +| Key | Effect | | |
| 152 | +| --- | --- | | |
| 153 | +| `Shift-PgUp`, `Shift-PgDn` | Read back through the output | | |
| 154 | +| `Ctrl-W` | Close the window | | |
| 155 | +| Anything else | Reaches the editor, not the dead shell | | |
| 156 | + | |
| 157 | +### `output = "editor"` | |
| 158 | + | |
| 159 | +| Property | Value | | |
| 160 | +| --- | --- | | |
| 161 | +| Shows | A popup while it runs, as above | | |
| 162 | +| On closing the popup | An editing window holding the output, titled with the command | | |
| 163 | +| Filled | Once, when the command has finished — not as it goes | | |
| 164 | +| The window | An ordinary editing window with no file name: searchable with `Ctrl-F`, and `Save as` keeps it | | |
| 165 | + | |
| 166 | +## Reloading after a command | |
| 167 | + | |
| 168 | +When a command finishes, every open file is considered. | |
| 169 | + | |
| 170 | +| The file | What happens | | |
| 171 | +| --- | --- | | |
| 172 | +| Unmodified, and changed on disk | Re-read; its syntax is re-decided and its title refreshed | | |
| 173 | +| Unmodified, and unchanged on disk | Left alone, not counted | | |
| 174 | +| Has unsaved changes | Left alone and counted as skipped | | |
| 175 | +| Has never been named | Left alone | | |
| 176 | +| Has gone from disk | Left alone | | |
| 177 | + | |
| 178 | +The cursor stays where it was, clamped into whatever the file now holds. The undo history is discarded, because undoing back past a reload would restore text the file no longer has. | |
| 179 | + | |
| 180 | +The project tree is refreshed at the same moment — which is how a file written by `golo new` appears in it. | |
| 181 | + | |
| 182 | +| Status bar | When | | |
| 183 | +| --- | --- | | |
| 184 | +| `Running <command>` | The window opens | | |
| 185 | +| `Reloaded 2 files` | Two files were re-read, none skipped | | |
| 186 | +| `Reloaded 2 files; 1 file with unsaved changes left alone` | Some were skipped | | |
| 187 | +| `Command finished; 1 file with unsaved changes left alone` | Nothing was re-read, something was skipped | | |
| 188 | + | |
| 189 | +## Errors | |
| 190 | + | |
| 191 | +| Message | Cause | | |
| 192 | +| --- | --- | | |
| 193 | +| `Cannot read tools` in the menu | The file is present but not valid TOML, or holds a tool with no name or no command | | |
| 194 | +| `Already there: .turbo-golo/tools.toml` | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. | | |
| 195 | +| `This project has no .turbo-golo/tools.toml yet.` | Opening in a project that has none, likewise | | |
| 196 | +| `Cannot tell which directory this is: …` | The working directory could not be read | | |
| 197 | +| `Terminal windows are not supported on this platform yet` | Running a command in a terminal needs a pseudo-terminal, which Linux, macOS and Windows have; see [Terminal windows](terminal.md) | | |
| 198 | + | |
| 199 | +## Asking for a value | |
| 200 | + | |
| 201 | +A `{{label}}` anywhere in a command is a value the editor asks for before it runs, in a box titled after the tool. The text between the braces is what the box asks for. | |
| 202 | + | |
| 203 | +| Written | Asked for | Substituted | | |
| 204 | +| --- | --- | --- | | |
| 205 | +| `{{script, e.g. main.golo}}` | `script, e.g. main.golo` | shell-quoted | | |
| 206 | +| `{{arguments...}}` | `arguments` | verbatim | | |
| 207 | + | |
| 208 | +A value is **shell-quoted** by default, so a path with a space in it stays one argument. A trailing `...` inside the braces asks for it verbatim instead, which is how one field can stand for several arguments. | |
| 209 | + | |
| 210 | +```toml | |
| 211 | +[[tool]] | |
| 212 | +name = "Run with ~a~rguments" | |
| 213 | +command = "golo main.golo {{arguments...}}" | |
| 214 | +output = "terminal" | |
| 215 | +``` | |
| 216 | + | |
| 217 | +| Rule | Behaviour | | |
| 218 | +| --- | --- | | |
| 219 | +| Several placeholders | One box, one field each, in the order they appear in the command | | |
| 220 | +| The same label twice | One field; every occurrence gets what is typed into it | | |
| 221 | +| A label written both ways | Asked for once; each occurrence honours its own braces | | |
| 222 | +| Escape, or Cancel | The command does not run | | |
| 223 | +| A field left empty | Substituted as empty — the command reports its own complaint | | |
| 224 | +| Running the tool again | The box starts from what was typed last time, for this session only | | |
| 225 | +| More fields than fit on screen | Refused, with a message saying how many fit | | |
| 226 | + | |
| 227 | +**Double braces, not single.** `awk '{print $1}'` and `find . -exec rm {} +` are ordinary commands, and a single-brace syntax would read the first as a request for a value called `print $1`. | |
| 228 | + | |
| 229 | +Nothing is written to disk. A value somebody typed this afternoon is not a decision the project made, so it does not go in the project's own directory. | |
| 230 | + | |
| 231 | +### Errors | |
| 232 | + | |
| 233 | +| Error | Cause | | |
| 234 | +| --- | --- | | |
| 235 | +| `tool "X": "{{script" is never closed` | An opening `{{` with no `}}` after it | | |
| 236 | +| `tool "X": {{}} asks for a value but does not say what it is` | A placeholder with no label, or one that is only `...` | | |
| 237 | + | |
| 238 | +Both are refused when the file is read, so a half-typed placeholder never reaches the shell with its braces still in it. | |
| 239 | + | |
| 240 | +## See also | |
| 241 | + | |
| 242 | +- [How to run Golo commands from the editor](../how-to/run-golo-commands.md) | |
| 243 | +- [Golo tools](../explanation/golo-tools.md) | |
| 244 | +- [Terminal windows](terminal.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,244 @@ | |||
| 1 | +# Reference: Golo tools | ||
| 2 | + | ||
| 3 | +> Neutral description of `.turbo-golo/tools.toml`, the Golo menu, and what running a command does. | ||
| 4 | + | ||
| 5 | +## File | ||
| 6 | + | ||
| 7 | +| Property | Value | | ||
| 8 | +| --- | --- | | ||
| 9 | +| Path | `./.turbo-golo/tools.toml` | | ||
| 10 | +| Search | The working directory only. Parent directories are **not** searched. | | ||
| 11 | +| Read | Every time one of its menus opens, for the items | | ||
| 12 | +| Re-read | Whenever the file's size or modification time changes, for the **set** of menus | | ||
| 13 | +| Missing file | Not an error | | ||
| 14 | +| Unreadable file | An error, shown in the menu | | ||
| 15 | +| User-level file | **None.** Unlike snippets, there is no `~/.config/turbo-golo/tools.toml`. | | ||
| 16 | + | ||
| 17 | +## File format | ||
| 18 | + | ||
| 19 | +One `[[tool]]` table per command. | ||
| 20 | + | ||
| 21 | +| Key | Type | Required | Description | | ||
| 22 | +| --- | --- | --- | --- | | ||
| 23 | +| `name` | string | yes | What the menu shows. May carry a hot key written with tildes, as in `"~T~est"`. | | ||
| 24 | +| `command` | string | yes | The shell command to run | | ||
| 25 | +| `output` | string | no | Where its output goes: `popup`, `terminal` or `editor`. Absent means `popup`. | | ||
| 26 | +| `menu` | string | no | Which menu it appears in. Absent means `Golo`. Any name; the menu is created for you. May carry a hot key written with tildes. | | ||
| 27 | + | ||
| 28 | +`menu` is not checked against a list, because there is no list: a name that no other tool uses simply creates a menu. A tool with no `name`, no `command`, or an `output` naming something that does not exist makes the whole file an error. An unknown `output` is **refused rather than corrected**: `"termnial"` would otherwise look as though it had worked while sending the output somewhere else. | ||
| 29 | + | ||
| 30 | +### Example | ||
| 31 | + | ||
| 32 | +```toml | ||
| 33 | +[[tool]] | ||
| 34 | +name = "~T~est" | ||
| 35 | +command = "golo --test" | ||
| 36 | +output = "popup" | ||
| 37 | + | ||
| 38 | +[[tool]] | ||
| 39 | +name = "~E~cho" | ||
| 40 | +command = "echo TADA" | ||
| 41 | +output = "terminal" | ||
| 42 | +menu = "Tools" | ||
| 43 | +``` | ||
| 44 | + | ||
| 45 | +## The starter file | ||
| 46 | + | ||
| 47 | +**Golo ▸ Create tools file** writes these nine, in this order: | ||
| 48 | + | ||
| 49 | +| Name | Command | Output | Menu | | ||
| 50 | +| --- | --- | --- | --- | | ||
| 51 | +| `~R~un` | `golo {{script, e.g. main.golo}}` | `terminal` | Golo | | ||
| 52 | +| `~T~est` | `golo --test` | `popup` | Golo | | ||
| 53 | +| `Test ~o~ne` | `golo --test {{test file or directory}}` | `popup` | Golo | | ||
| 54 | +| `~D~ebug` | `golo --debug {{script, e.g. main.golo}}` | `terminal` | Golo | | ||
| 55 | +| `R~E~PL` | `golo` | `terminal` | Golo | | ||
| 56 | +| `~N~ew script` | `golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}` | `popup` | Golo | | ||
| 57 | +| `~B~uild native` | `gogolo build -o {{output executable}} {{script, e.g. main.golo}}` | `popup` | Golo | | ||
| 58 | +| `Build ~w~asm` | `wagolo build -target={{target: wasi, js or wasip2}} -o {{output .wasm}} {{script, e.g. main.golo}}` | `popup` | Golo | | ||
| 59 | +| `~E~cho` | `echo 🎉 tada!` | `terminal` | Tools | | ||
| 60 | + | ||
| 61 | +`Run` comes first because Golo is a scripting language and running the file is what a Golo programmer does most. Six of them ask for a value before they run — Golo has no manifest, so every command that touches a file has to be told which one — and one names a `menu` of its own. Those two features are invisible unless the starter file shows them. | ||
| 62 | + | ||
| 63 | +`Run`, `Debug` and `REPL` get a terminal: the first two may read the keyboard, and the third is nothing else. `Build native` needs the Go toolchain on `PATH`; `Build wasm` needs TinyGo, and `wasm-tools` for the `wasip2` target. | ||
| 64 | + | ||
| 65 | +Every tool names its `output`, including the ones that name the default: the key is the interesting part of the format, and a file where it appears once is a file where nobody notices it exists. | ||
| 66 | + | ||
| 67 | +The item is greyed out once the project has a tools file, so it cannot overwrite one. The file is written through a temporary file in the same directory, renamed into place. | ||
| 68 | + | ||
| 69 | +## The Golo menu | ||
| 70 | + | ||
| 71 | +Always on the bar, whether or not a tools file exists. Its hot key is `Alt-G`. | ||
| 72 | + | ||
| 73 | +| Item | Condition | | ||
| 74 | +| --- | --- | | ||
| 75 | +| One line per tool with no `menu`, in file order | The file holds at least one | | ||
| 76 | +| `Cannot read tools`, greyed out | The file is present but unreadable | | ||
| 77 | +| `Create tools file` | The project has no tools file | | ||
| 78 | +| `Open tools file` | The project has one | | ||
| 79 | + | ||
| 80 | +## Menus a tool asks for | ||
| 81 | + | ||
| 82 | +A `menu` naming anything other than `Golo` puts a menu of that name on the bar. | ||
| 83 | + | ||
| 84 | +| Property | Value | | ||
| 85 | +| --- | --- | | ||
| 86 | +| Position | Between Golo and Help | | ||
| 87 | +| Order | The order each name first appears in the file | | ||
| 88 | +| Items | One line per tool naming that menu, in file order. Nothing else — `Create tools file` and `Open tools file` stay in Golo. | | ||
| 89 | +| Unreadable file | No menus at all; the Golo menu carries the error | | ||
| 90 | +| While the editor runs | Added, removed and renamed as the file changes, without restarting | | ||
| 91 | + | ||
| 92 | +### Hot keys | ||
| 93 | + | ||
| 94 | +Assigned automatically, because a name from a file cannot be checked against the fixed menus in advance. | ||
| 95 | + | ||
| 96 | +| Case | Result | | ||
| 97 | +| --- | --- | | ||
| 98 | +| No tildes in the name | The first letter no other menu has claimed is marked. `Format` becomes `For~m~at`: `F` is File's, `o` is Options', `r` is Run's. | | ||
| 99 | +| Tildes naming a free letter | Kept as written. `Doc~k~er` answers to `Alt-K`. | | ||
| 100 | +| Tildes naming a taken letter | Dropped, and a free letter chosen instead. `~F~oo` becomes `F~o~o`. | | ||
| 101 | +| Every letter taken | No hot key. `F10` and the mouse still open it. | | ||
| 102 | + | ||
| 103 | +The letters the editor's own menus hold are `F`, `E`, `S`, `R`, `C`, `O`, `W`, `N` (Snippets), `G` (Golo) and `H`. | ||
| 104 | + | ||
| 105 | +## Running a command | ||
| 106 | + | ||
| 107 | +Common to every output: | ||
| 108 | + | ||
| 109 | +| Property | Value | | ||
| 110 | +| --- | --- | | ||
| 111 | +| Shell | `/bin/sh -c "<command>"` on Linux and macOS; `cmd.exe /S /C "<command>"` — the shell `%COMSPEC%` names — on Windows | | ||
| 112 | +| Directory | The directory the editor was started in | | ||
| 113 | +| Standard error | Merged into standard output, in the order the command wrote them | | ||
| 114 | + | ||
| 115 | +Going through a shell means pipes, globs, `&&` and `;` all work, so one tool can be a sequence. On Windows the shell is cmd.exe, which knows `&&`, `|` and `>` but does not expand globs, and where `;` is not a separator. | ||
| 116 | + | ||
| 117 | +### `output = "popup"` | ||
| 118 | + | ||
| 119 | +| Property | Value | | ||
| 120 | +| --- | --- | | ||
| 121 | +| Opens | Immediately, before the command has finished | | ||
| 122 | +| Modal | Yes: nothing else in the editor can be used while it is up | | ||
| 123 | +| Fills in | As output arrives, following it until you scroll back | | ||
| 124 | +| Title while running | `<command> — running` | | ||
| 125 | +| Title when finished | `<command> — ok`, or `<command> — exit <n>` | | ||
| 126 | +| Empty output, finished | Shows `(no output)` | | ||
| 127 | +| Empty output, running | Shows nothing | | ||
| 128 | +| Output cap | 10000 lines; past it the oldest go and a `… n earlier lines dropped …` line says so | | ||
| 129 | + | ||
| 130 | +| Key | Effect | | ||
| 131 | +| --- | --- | | ||
| 132 | +| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Read through the output | | ||
| 133 | +| Wheel | The same | | ||
| 134 | +| `Escape`, `Enter`, **Close** | Close it, **stopping the command** if it is still running | | ||
| 135 | + | ||
| 136 | +Closing stops the command because there is no other way to interrupt one whose output is not in a terminal. | ||
| 137 | + | ||
| 138 | +### `output = "terminal"` | ||
| 139 | + | ||
| 140 | +| Property | Value | | ||
| 141 | +| --- | --- | | ||
| 142 | +| Window | A terminal window of its own, titled with the command | | ||
| 143 | +| Environment | The editor's own, with `TERM` set to `xterm-256color` | | ||
| 144 | +| After it exits | The window stays, showing its output | | ||
| 145 | +| Modal | No: the editor carries on beside it | | ||
| 146 | + | ||
| 147 | +Because it is a real terminal, colours, paging, `Ctrl-C` and reading from the keyboard all work — `golo --test`'s green ticks, `readln` in a script, the REPL's prompt. See [Terminal windows](terminal.md). | ||
| 148 | + | ||
| 149 | +Keys in a **finished** terminal window: | ||
| 150 | + | ||
| 151 | +| Key | Effect | | ||
| 152 | +| --- | --- | | ||
| 153 | +| `Shift-PgUp`, `Shift-PgDn` | Read back through the output | | ||
| 154 | +| `Ctrl-W` | Close the window | | ||
| 155 | +| Anything else | Reaches the editor, not the dead shell | | ||
| 156 | + | ||
| 157 | +### `output = "editor"` | ||
| 158 | + | ||
| 159 | +| Property | Value | | ||
| 160 | +| --- | --- | | ||
| 161 | +| Shows | A popup while it runs, as above | | ||
| 162 | +| On closing the popup | An editing window holding the output, titled with the command | | ||
| 163 | +| Filled | Once, when the command has finished — not as it goes | | ||
| 164 | +| The window | An ordinary editing window with no file name: searchable with `Ctrl-F`, and `Save as` keeps it | | ||
| 165 | + | ||
| 166 | +## Reloading after a command | ||
| 167 | + | ||
| 168 | +When a command finishes, every open file is considered. | ||
| 169 | + | ||
| 170 | +| The file | What happens | | ||
| 171 | +| --- | --- | | ||
| 172 | +| Unmodified, and changed on disk | Re-read; its syntax is re-decided and its title refreshed | | ||
| 173 | +| Unmodified, and unchanged on disk | Left alone, not counted | | ||
| 174 | +| Has unsaved changes | Left alone and counted as skipped | | ||
| 175 | +| Has never been named | Left alone | | ||
| 176 | +| Has gone from disk | Left alone | | ||
| 177 | + | ||
| 178 | +The cursor stays where it was, clamped into whatever the file now holds. The undo history is discarded, because undoing back past a reload would restore text the file no longer has. | ||
| 179 | + | ||
| 180 | +The project tree is refreshed at the same moment — which is how a file written by `golo new` appears in it. | ||
| 181 | + | ||
| 182 | +| Status bar | When | | ||
| 183 | +| --- | --- | | ||
| 184 | +| `Running <command>` | The window opens | | ||
| 185 | +| `Reloaded 2 files` | Two files were re-read, none skipped | | ||
| 186 | +| `Reloaded 2 files; 1 file with unsaved changes left alone` | Some were skipped | | ||
| 187 | +| `Command finished; 1 file with unsaved changes left alone` | Nothing was re-read, something was skipped | | ||
| 188 | + | ||
| 189 | +## Errors | ||
| 190 | + | ||
| 191 | +| Message | Cause | | ||
| 192 | +| --- | --- | | ||
| 193 | +| `Cannot read tools` in the menu | The file is present but not valid TOML, or holds a tool with no name or no command | | ||
| 194 | +| `Already there: .turbo-golo/tools.toml` | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. | | ||
| 195 | +| `This project has no .turbo-golo/tools.toml yet.` | Opening in a project that has none, likewise | | ||
| 196 | +| `Cannot tell which directory this is: …` | The working directory could not be read | | ||
| 197 | +| `Terminal windows are not supported on this platform yet` | Running a command in a terminal needs a pseudo-terminal, which Linux, macOS and Windows have; see [Terminal windows](terminal.md) | | ||
| 198 | + | ||
| 199 | +## Asking for a value | ||
| 200 | + | ||
| 201 | +A `{{label}}` anywhere in a command is a value the editor asks for before it runs, in a box titled after the tool. The text between the braces is what the box asks for. | ||
| 202 | + | ||
| 203 | +| Written | Asked for | Substituted | | ||
| 204 | +| --- | --- | --- | | ||
| 205 | +| `{{script, e.g. main.golo}}` | `script, e.g. main.golo` | shell-quoted | | ||
| 206 | +| `{{arguments...}}` | `arguments` | verbatim | | ||
| 207 | + | ||
| 208 | +A value is **shell-quoted** by default, so a path with a space in it stays one argument. A trailing `...` inside the braces asks for it verbatim instead, which is how one field can stand for several arguments. | ||
| 209 | + | ||
| 210 | +```toml | ||
| 211 | +[[tool]] | ||
| 212 | +name = "Run with ~a~rguments" | ||
| 213 | +command = "golo main.golo {{arguments...}}" | ||
| 214 | +output = "terminal" | ||
| 215 | +``` | ||
| 216 | + | ||
| 217 | +| Rule | Behaviour | | ||
| 218 | +| --- | --- | | ||
| 219 | +| Several placeholders | One box, one field each, in the order they appear in the command | | ||
| 220 | +| The same label twice | One field; every occurrence gets what is typed into it | | ||
| 221 | +| A label written both ways | Asked for once; each occurrence honours its own braces | | ||
| 222 | +| Escape, or Cancel | The command does not run | | ||
| 223 | +| A field left empty | Substituted as empty — the command reports its own complaint | | ||
| 224 | +| Running the tool again | The box starts from what was typed last time, for this session only | | ||
| 225 | +| More fields than fit on screen | Refused, with a message saying how many fit | | ||
| 226 | + | ||
| 227 | +**Double braces, not single.** `awk '{print $1}'` and `find . -exec rm {} +` are ordinary commands, and a single-brace syntax would read the first as a request for a value called `print $1`. | ||
| 228 | + | ||
| 229 | +Nothing is written to disk. A value somebody typed this afternoon is not a decision the project made, so it does not go in the project's own directory. | ||
| 230 | + | ||
| 231 | +### Errors | ||
| 232 | + | ||
| 233 | +| Error | Cause | | ||
| 234 | +| --- | --- | | ||
| 235 | +| `tool "X": "{{script" is never closed` | An opening `{{` with no `}}` after it | | ||
| 236 | +| `tool "X": {{}} asks for a value but does not say what it is` | A placeholder with no label, or one that is only `...` | | ||
| 237 | + | ||
| 238 | +Both are refused when the file is read, so a half-typed placeholder never reaches the shell with its braces still in it. | ||
| 239 | + | ||
| 240 | +## See also | ||
| 241 | + | ||
| 242 | +- [How to run Golo commands from the editor](../how-to/run-golo-commands.md) | ||
| 243 | +- [Golo tools](../explanation/golo-tools.md) | ||
| 244 | +- [Terminal windows](terminal.md) | ||
added
docs/en/reference/keyboard.md +187 -0 | new file mode 100644 | ||
| @@ -0,0 +1,187 @@ | ||
| 1 | +# Reference: keyboard | |
| 2 | + | |
| 3 | +> Complete list of the keys Turbo Golo answers to, grouped by what has the focus. | |
| 4 | + | |
| 5 | +Where two spellings exist, both work: the Turbo C one and the modern one. | |
| 6 | + | |
| 7 | +## Global | |
| 8 | + | |
| 9 | +Handled wherever the focus is, unless a dialog or the completion popup is open. | |
| 10 | + | |
| 11 | +| Key | Action | | |
| 12 | +| --- | --- | | |
| 13 | +| `F1` | Describe the symbol under the cursor; with no file open, show the keyboard help | | |
| 14 | +| `F2` | Save | | |
| 15 | +| `F3` | Open | | |
| 16 | +| `F4` | New | | |
| 17 | +| `F6` | Next window | | |
| 18 | +| `F7` | Find next | | |
| 19 | +| `Shift-F7` | Find previous | | |
| 20 | +| `F8` | Open a terminal window | | |
| 21 | +| `F9` | Open the project tree | | |
| 22 | +| `F10` | Open the menu bar | | |
| 23 | +| `F12` | Go to definition | | |
| 24 | +| `Shift-F12` | Find references — with `golo lsp`, the declaration and every call within the file | | |
| 25 | +| `Ctrl-T` | Find a symbol anywhere in the project — with `golo lsp`, in every `.golo` file under the project root, open or not | | |
| 26 | +| `Ctrl-F` | Find | | |
| 27 | +| `Ctrl-G` | Go to line | | |
| 28 | +| `Ctrl-W` | Close the current window | | |
| 29 | +| `Alt-X` | Exit | | |
| 30 | +| `Alt-1` … `Alt-9` | Bring window 1…9 forward | | |
| 31 | +| `Alt-0` | List the open windows | | |
| 32 | +| `Alt-N` | Open the Snippets menu | | |
| 33 | +| `Alt-G` | Open the Golo menu | | |
| 34 | +| `Alt-<letter>` | Open the menu whose title carries that letter | | |
| 35 | + | |
| 36 | +A menu the project's tools file adds gets its letter assigned rather than fixed, so it is never one of the above. The rules are in [Golo tools](golo-tools.md#hot-keys). | |
| 37 | + | |
| 38 | +## Editing | |
| 39 | + | |
| 40 | +Handled by the window that has the focus. | |
| 41 | + | |
| 42 | +### Movement | |
| 43 | + | |
| 44 | +| Key | Action | | |
| 45 | +| --- | --- | | |
| 46 | +| `←` `→` `↑` `↓` | One character or one line | | |
| 47 | +| `Ctrl-←` `Ctrl-→` | Start of the previous / next word | | |
| 48 | +| `Home` `End` | Start / end of the line | | |
| 49 | +| `Ctrl-Home` `Ctrl-End` | Start / end of the file | | |
| 50 | +| `PgUp` `PgDn` | One screenful | | |
| 51 | +| `Shift` + any of the above | The same movement, extending the selection | | |
| 52 | + | |
| 53 | +### Changing the text | |
| 54 | + | |
| 55 | +| Key | Action | | |
| 56 | +| --- | --- | | |
| 57 | +| any printable character | Insert it, replacing the selection | | |
| 58 | +| `Enter` | Split the line, copying the current line's indentation | | |
| 59 | +| `Backspace` | Delete the selection, or the character before the cursor | | |
| 60 | +| `Delete` | Delete the selection, or the character under the cursor | | |
| 61 | +| `Tab` | Insert a tab; with a selection, indent every line it touches | | |
| 62 | +| `Shift-Tab` | Remove one level of indentation from every line the selection touches | | |
| 63 | + | |
| 64 | +### Clipboard and history | |
| 65 | + | |
| 66 | +| Key | Also | Action | | |
| 67 | +| --- | --- | --- | | |
| 68 | +| `Ctrl-C` | `Ctrl-Ins` | Copy the selection | | |
| 69 | +| `Ctrl-X` | `Shift-Del` | Cut the selection | | |
| 70 | +| `Ctrl-V` | `Shift-Ins` | Paste | | |
| 71 | +| `Ctrl-A` | | Select the whole file | | |
| 72 | +| `Ctrl-Z` | | Undo | | |
| 73 | +| `Ctrl-R` | | Redo | | |
| 74 | +| `Ctrl-N` | | Insert a blank line above the cursor | | |
| 75 | +| `Ctrl-Y` | | Delete the line the cursor is on | | |
| 76 | + | |
| 77 | +A run of typed characters, or a run of backspaces, is a **single** undo step. Moving the cursor ends the run. | |
| 78 | + | |
| 79 | +### Language server | |
| 80 | + | |
| 81 | +| Key | Action | | |
| 82 | +| --- | --- | | |
| 83 | +| `Ctrl-Space` | Ask for a completion list | | |
| 84 | +| `.` | Ask for a completion list, as a side effect of typing it. Golo's method calls use `:`, which is not a trigger — press `Ctrl-Space` there. | | |
| 85 | +| `F1` | Describe the symbol under the cursor | | |
| 86 | +| `F12` | Go to the declaration | | |
| 87 | + | |
| 88 | +## Menu bar | |
| 89 | + | |
| 90 | +Once a menu is open. | |
| 91 | + | |
| 92 | +| Key | Action | | |
| 93 | +| --- | --- | | |
| 94 | +| `←` `→` | Previous / next menu | | |
| 95 | +| `↑` `↓` | Previous / next item, skipping separators and disabled items | | |
| 96 | +| `Enter` | Run the highlighted item | | |
| 97 | +| `<letter>` | Run the item whose label carries that letter | | |
| 98 | +| `Escape` | Close the menu | | |
| 99 | + | |
| 100 | +An item marked `▶` opens a submenu instead of running: | |
| 101 | + | |
| 102 | +| Key | Action | | |
| 103 | +| --- | --- | | |
| 104 | +| `→`, `Enter` | Open the highlighted submenu; `→` on an item without one moves to the next menu | | |
| 105 | +| `←` | Step back out to the parent menu | | |
| 106 | +| `Escape` | Close the whole menu, wherever you are | | |
| 107 | +| `↑` `↓` | Walk the submenu | | |
| 108 | +| `<letter>` | Run the submenu item whose label carries that letter | | |
| 109 | + | |
| 110 | +Any other key is swallowed, so stray typing never reaches the file behind. | |
| 111 | + | |
| 112 | +## Dialogs | |
| 113 | + | |
| 114 | +| Key | Action | | |
| 115 | +| --- | --- | | |
| 116 | +| `Tab` / `Shift-Tab` | Next / previous control | | |
| 117 | +| `↑` `↓` | Walk the focused list; when the focused control has no use for them, the next / previous control | | |
| 118 | +| `Enter` | Press the default button, from wherever the focus is | | |
| 119 | +| `Escape` | Cancel | | |
| 120 | +| `Alt-<letter>` | Press the button whose label carries that letter | | |
| 121 | +| `Ctrl-U` | Clear the focused input field | | |
| 122 | + | |
| 123 | +A dialog is modal: every key it does not use is swallowed rather than passed to the editor behind it. | |
| 124 | + | |
| 125 | +### The Open and Save As box | |
| 126 | + | |
| 127 | +| | | | |
| 128 | +| --- | --- | | |
| 129 | +| Focus on opening | The **Name** field, so a name can be typed straight away. The first `↓` therefore moves the focus to the list; the second moves the highlight. | | |
| 130 | +| Moving the highlight | Puts that entry's name into the **Name** field, so the field always says what **OK** will act on. Highlighting `../` clears it. | | |
| 131 | +| `Enter` on the list | Opens the highlighted file, or browses into the highlighted directory | | |
| 132 | +| **OK** | Acts on the **Name** field; when the field is empty, acts on whatever the list has highlighted | | |
| 133 | +| A name that is a directory | Browses into it rather than closing the dialog | | |
| 134 | +| Double click | The same as `Enter` on that entry | | |
| 135 | + | |
| 136 | +Dot-files are not listed. Directories come before files, each group sorted, with `../` first. | |
| 137 | + | |
| 138 | +## Completion popup | |
| 139 | + | |
| 140 | +| Key | Action | | |
| 141 | +| --- | --- | | |
| 142 | +| `↑` `↓` | Previous / next suggestion | | |
| 143 | +| `PgUp` `PgDn` | Eight at a time | | |
| 144 | +| `Enter`, `Tab` | Accept the highlighted suggestion | | |
| 145 | +| `Escape` | Dismiss the list | | |
| 146 | +| any printable character | Passed through to the editor; the list narrows to what still matches | | |
| 147 | + | |
| 148 | +## Terminal windows | |
| 149 | + | |
| 150 | +A terminal window in front gets **every key except** the function keys, `Alt-X` and `Alt-0`…`Alt-9`, which stay with the editor so there is always a way out of a full-screen program. `Ctrl-C`, `Ctrl-W`, `Ctrl-F` and `Alt-<letter>` therefore reach the shell rather than the editor. | |
| 151 | + | |
| 152 | +| Key | Action | | |
| 153 | +| --- | --- | | |
| 154 | +| `Shift-PgUp` `Shift-PgDn` | Read back / forward one screenful through the history | | |
| 155 | +| anything else not reserved above | Sent to the shell, returning the view to the live screen | | |
| 156 | + | |
| 157 | +The exact byte each key sends is in [Terminal windows](terminal.md). | |
| 158 | + | |
| 159 | +## Project tree | |
| 160 | + | |
| 161 | +Handled when the tree window has the focus. The full rules are in [Project tree](project-tree.md). | |
| 162 | + | |
| 163 | +| Key | Action | | |
| 164 | +| --- | --- | | |
| 165 | +| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Move the highlight | | |
| 166 | +| `→` | Expand a closed directory, else step to the next row | | |
| 167 | +| `←` | Collapse an open directory, else step out to its directory | | |
| 168 | +| `Enter` | Open a file; expand or collapse a directory | | |
| 169 | +| `F5`, `Ctrl-R` | Re-read the project | | |
| 170 | + | |
| 171 | +## Mouse | |
| 172 | + | |
| 173 | +| Action | Effect | | |
| 174 | +| --- | --- | | |
| 175 | +| Click in the text | Place the cursor | | |
| 176 | +| Drag in the text | Select | | |
| 177 | +| Wheel | Scroll three lines | | |
| 178 | +| Click a menu title | Open or close that menu | | |
| 179 | +| Click a status-bar hint | Run it | | |
| 180 | +| Click a window | Bring it forward | | |
| 181 | +| Drag a title bar | Move the window | | |
| 182 | +| Drag the bottom-right corner | Resize the window | | |
| 183 | +| Click `[x]` | Close the window | | |
| 184 | +| Click `[■]` | Fill the desktop with the window | | |
| 185 | +| Click `[▬]` | Put a filled window back where it was | | |
| 186 | +| Wheel over a terminal | Scroll three lines through its history | | |
| 187 | +| Click a tree row | Highlight it; a second click opens it | | |
| new file mode 100644 | |||
| @@ -0,0 +1,187 @@ | |||
| 1 | +# Reference: keyboard | ||
| 2 | + | ||
| 3 | +> Complete list of the keys Turbo Golo answers to, grouped by what has the focus. | ||
| 4 | + | ||
| 5 | +Where two spellings exist, both work: the Turbo C one and the modern one. | ||
| 6 | + | ||
| 7 | +## Global | ||
| 8 | + | ||
| 9 | +Handled wherever the focus is, unless a dialog or the completion popup is open. | ||
| 10 | + | ||
| 11 | +| Key | Action | | ||
| 12 | +| --- | --- | | ||
| 13 | +| `F1` | Describe the symbol under the cursor; with no file open, show the keyboard help | | ||
| 14 | +| `F2` | Save | | ||
| 15 | +| `F3` | Open | | ||
| 16 | +| `F4` | New | | ||
| 17 | +| `F6` | Next window | | ||
| 18 | +| `F7` | Find next | | ||
| 19 | +| `Shift-F7` | Find previous | | ||
| 20 | +| `F8` | Open a terminal window | | ||
| 21 | +| `F9` | Open the project tree | | ||
| 22 | +| `F10` | Open the menu bar | | ||
| 23 | +| `F12` | Go to definition | | ||
| 24 | +| `Shift-F12` | Find references — with `golo lsp`, the declaration and every call within the file | | ||
| 25 | +| `Ctrl-T` | Find a symbol anywhere in the project — with `golo lsp`, in every `.golo` file under the project root, open or not | | ||
| 26 | +| `Ctrl-F` | Find | | ||
| 27 | +| `Ctrl-G` | Go to line | | ||
| 28 | +| `Ctrl-W` | Close the current window | | ||
| 29 | +| `Alt-X` | Exit | | ||
| 30 | +| `Alt-1` … `Alt-9` | Bring window 1…9 forward | | ||
| 31 | +| `Alt-0` | List the open windows | | ||
| 32 | +| `Alt-N` | Open the Snippets menu | | ||
| 33 | +| `Alt-G` | Open the Golo menu | | ||
| 34 | +| `Alt-<letter>` | Open the menu whose title carries that letter | | ||
| 35 | + | ||
| 36 | +A menu the project's tools file adds gets its letter assigned rather than fixed, so it is never one of the above. The rules are in [Golo tools](golo-tools.md#hot-keys). | ||
| 37 | + | ||
| 38 | +## Editing | ||
| 39 | + | ||
| 40 | +Handled by the window that has the focus. | ||
| 41 | + | ||
| 42 | +### Movement | ||
| 43 | + | ||
| 44 | +| Key | Action | | ||
| 45 | +| --- | --- | | ||
| 46 | +| `←` `→` `↑` `↓` | One character or one line | | ||
| 47 | +| `Ctrl-←` `Ctrl-→` | Start of the previous / next word | | ||
| 48 | +| `Home` `End` | Start / end of the line | | ||
| 49 | +| `Ctrl-Home` `Ctrl-End` | Start / end of the file | | ||
| 50 | +| `PgUp` `PgDn` | One screenful | | ||
| 51 | +| `Shift` + any of the above | The same movement, extending the selection | | ||
| 52 | + | ||
| 53 | +### Changing the text | ||
| 54 | + | ||
| 55 | +| Key | Action | | ||
| 56 | +| --- | --- | | ||
| 57 | +| any printable character | Insert it, replacing the selection | | ||
| 58 | +| `Enter` | Split the line, copying the current line's indentation | | ||
| 59 | +| `Backspace` | Delete the selection, or the character before the cursor | | ||
| 60 | +| `Delete` | Delete the selection, or the character under the cursor | | ||
| 61 | +| `Tab` | Insert a tab; with a selection, indent every line it touches | | ||
| 62 | +| `Shift-Tab` | Remove one level of indentation from every line the selection touches | | ||
| 63 | + | ||
| 64 | +### Clipboard and history | ||
| 65 | + | ||
| 66 | +| Key | Also | Action | | ||
| 67 | +| --- | --- | --- | | ||
| 68 | +| `Ctrl-C` | `Ctrl-Ins` | Copy the selection | | ||
| 69 | +| `Ctrl-X` | `Shift-Del` | Cut the selection | | ||
| 70 | +| `Ctrl-V` | `Shift-Ins` | Paste | | ||
| 71 | +| `Ctrl-A` | | Select the whole file | | ||
| 72 | +| `Ctrl-Z` | | Undo | | ||
| 73 | +| `Ctrl-R` | | Redo | | ||
| 74 | +| `Ctrl-N` | | Insert a blank line above the cursor | | ||
| 75 | +| `Ctrl-Y` | | Delete the line the cursor is on | | ||
| 76 | + | ||
| 77 | +A run of typed characters, or a run of backspaces, is a **single** undo step. Moving the cursor ends the run. | ||
| 78 | + | ||
| 79 | +### Language server | ||
| 80 | + | ||
| 81 | +| Key | Action | | ||
| 82 | +| --- | --- | | ||
| 83 | +| `Ctrl-Space` | Ask for a completion list | | ||
| 84 | +| `.` | Ask for a completion list, as a side effect of typing it. Golo's method calls use `:`, which is not a trigger — press `Ctrl-Space` there. | | ||
| 85 | +| `F1` | Describe the symbol under the cursor | | ||
| 86 | +| `F12` | Go to the declaration | | ||
| 87 | + | ||
| 88 | +## Menu bar | ||
| 89 | + | ||
| 90 | +Once a menu is open. | ||
| 91 | + | ||
| 92 | +| Key | Action | | ||
| 93 | +| --- | --- | | ||
| 94 | +| `←` `→` | Previous / next menu | | ||
| 95 | +| `↑` `↓` | Previous / next item, skipping separators and disabled items | | ||
| 96 | +| `Enter` | Run the highlighted item | | ||
| 97 | +| `<letter>` | Run the item whose label carries that letter | | ||
| 98 | +| `Escape` | Close the menu | | ||
| 99 | + | ||
| 100 | +An item marked `▶` opens a submenu instead of running: | ||
| 101 | + | ||
| 102 | +| Key | Action | | ||
| 103 | +| --- | --- | | ||
| 104 | +| `→`, `Enter` | Open the highlighted submenu; `→` on an item without one moves to the next menu | | ||
| 105 | +| `←` | Step back out to the parent menu | | ||
| 106 | +| `Escape` | Close the whole menu, wherever you are | | ||
| 107 | +| `↑` `↓` | Walk the submenu | | ||
| 108 | +| `<letter>` | Run the submenu item whose label carries that letter | | ||
| 109 | + | ||
| 110 | +Any other key is swallowed, so stray typing never reaches the file behind. | ||
| 111 | + | ||
| 112 | +## Dialogs | ||
| 113 | + | ||
| 114 | +| Key | Action | | ||
| 115 | +| --- | --- | | ||
| 116 | +| `Tab` / `Shift-Tab` | Next / previous control | | ||
| 117 | +| `↑` `↓` | Walk the focused list; when the focused control has no use for them, the next / previous control | | ||
| 118 | +| `Enter` | Press the default button, from wherever the focus is | | ||
| 119 | +| `Escape` | Cancel | | ||
| 120 | +| `Alt-<letter>` | Press the button whose label carries that letter | | ||
| 121 | +| `Ctrl-U` | Clear the focused input field | | ||
| 122 | + | ||
| 123 | +A dialog is modal: every key it does not use is swallowed rather than passed to the editor behind it. | ||
| 124 | + | ||
| 125 | +### The Open and Save As box | ||
| 126 | + | ||
| 127 | +| | | | ||
| 128 | +| --- | --- | | ||
| 129 | +| Focus on opening | The **Name** field, so a name can be typed straight away. The first `↓` therefore moves the focus to the list; the second moves the highlight. | | ||
| 130 | +| Moving the highlight | Puts that entry's name into the **Name** field, so the field always says what **OK** will act on. Highlighting `../` clears it. | | ||
| 131 | +| `Enter` on the list | Opens the highlighted file, or browses into the highlighted directory | | ||
| 132 | +| **OK** | Acts on the **Name** field; when the field is empty, acts on whatever the list has highlighted | | ||
| 133 | +| A name that is a directory | Browses into it rather than closing the dialog | | ||
| 134 | +| Double click | The same as `Enter` on that entry | | ||
| 135 | + | ||
| 136 | +Dot-files are not listed. Directories come before files, each group sorted, with `../` first. | ||
| 137 | + | ||
| 138 | +## Completion popup | ||
| 139 | + | ||
| 140 | +| Key | Action | | ||
| 141 | +| --- | --- | | ||
| 142 | +| `↑` `↓` | Previous / next suggestion | | ||
| 143 | +| `PgUp` `PgDn` | Eight at a time | | ||
| 144 | +| `Enter`, `Tab` | Accept the highlighted suggestion | | ||
| 145 | +| `Escape` | Dismiss the list | | ||
| 146 | +| any printable character | Passed through to the editor; the list narrows to what still matches | | ||
| 147 | + | ||
| 148 | +## Terminal windows | ||
| 149 | + | ||
| 150 | +A terminal window in front gets **every key except** the function keys, `Alt-X` and `Alt-0`…`Alt-9`, which stay with the editor so there is always a way out of a full-screen program. `Ctrl-C`, `Ctrl-W`, `Ctrl-F` and `Alt-<letter>` therefore reach the shell rather than the editor. | ||
| 151 | + | ||
| 152 | +| Key | Action | | ||
| 153 | +| --- | --- | | ||
| 154 | +| `Shift-PgUp` `Shift-PgDn` | Read back / forward one screenful through the history | | ||
| 155 | +| anything else not reserved above | Sent to the shell, returning the view to the live screen | | ||
| 156 | + | ||
| 157 | +The exact byte each key sends is in [Terminal windows](terminal.md). | ||
| 158 | + | ||
| 159 | +## Project tree | ||
| 160 | + | ||
| 161 | +Handled when the tree window has the focus. The full rules are in [Project tree](project-tree.md). | ||
| 162 | + | ||
| 163 | +| Key | Action | | ||
| 164 | +| --- | --- | | ||
| 165 | +| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Move the highlight | | ||
| 166 | +| `→` | Expand a closed directory, else step to the next row | | ||
| 167 | +| `←` | Collapse an open directory, else step out to its directory | | ||
| 168 | +| `Enter` | Open a file; expand or collapse a directory | | ||
| 169 | +| `F5`, `Ctrl-R` | Re-read the project | | ||
| 170 | + | ||
| 171 | +## Mouse | ||
| 172 | + | ||
| 173 | +| Action | Effect | | ||
| 174 | +| --- | --- | | ||
| 175 | +| Click in the text | Place the cursor | | ||
| 176 | +| Drag in the text | Select | | ||
| 177 | +| Wheel | Scroll three lines | | ||
| 178 | +| Click a menu title | Open or close that menu | | ||
| 179 | +| Click a status-bar hint | Run it | | ||
| 180 | +| Click a window | Bring it forward | | ||
| 181 | +| Drag a title bar | Move the window | | ||
| 182 | +| Drag the bottom-right corner | Resize the window | | ||
| 183 | +| Click `[x]` | Close the window | | ||
| 184 | +| Click `[■]` | Fill the desktop with the window | | ||
| 185 | +| Click `[▬]` | Put a filled window back where it was | | ||
| 186 | +| Wheel over a terminal | Scroll three lines through its history | | ||
| 187 | +| Click a tree row | Highlight it; a second click opens it | | ||
added
docs/en/reference/languages.md +294 -0 | new file mode 100644 | ||
| @@ -0,0 +1,294 @@ | ||
| 1 | +# Reference: languages coloured | |
| 2 | + | |
| 3 | +> Neutral description of which files Turbo Golo colours, how it decides, and what each scanner recognises. | |
| 4 | + | |
| 5 | +## Recognition | |
| 6 | + | |
| 7 | +A file's **extension** decides whenever it is one of these: | |
| 8 | + | |
| 9 | +| Extension | Language | | |
| 10 | +| --- | --- | | |
| 11 | +| `.golo` | Golo | | |
| 12 | +| `.toml` | TOML | | |
| 13 | +| `.yaml`, `.yml` | YAML | | |
| 14 | +| `.md`, `.markdown` | Markdown | | |
| 15 | +| `.js`, `.mjs`, `.cjs` | JavaScript | | |
| 16 | +| `.html`, `.htm` | HTML | | |
| 17 | +| `.xml`, `.xsd`, `.xsl`, `.xslt`, `.svg`, `.plist`, `.csproj`, `.pom` | XML | | |
| 18 | +| `.sh`, `.bash`, `.zsh` | Shell | | |
| 19 | +| `.dockerfile`, `.containerfile` | Dockerfile | | |
| 20 | + | |
| 21 | +Extensions are matched case-insensitively, and only the last one counts: `notes.golo.md` is Markdown, and `main.golo.backup` is not Golo. | |
| 22 | + | |
| 23 | +A file whose extension decides nothing is looked up by **name** next. Only files that carry no useful extension need this: | |
| 24 | + | |
| 25 | +| Name | Language | | |
| 26 | +| --- | --- | | |
| 27 | +| `Dockerfile`, `Containerfile` | Dockerfile | | |
| 28 | + | |
| 29 | +A name matches on the whole of it or on the part before the first dot, ignoring case — so `Dockerfile`, `dockerfile` and `Dockerfile.dev` are all recognised, while `Dockerfile.md` is Markdown, because the extension is consulted first. | |
| 30 | + | |
| 31 | +A file that neither table claims is read by its **first line**. A shebang naming `golo` makes it Golo: `#` opens a comment in Golo, so the interpreter reads the line as one, and a script installed without its extension and run as a command is Golo and nothing else. A shebang naming a shell — `sh`, `bash`, `zsh`, `dash` or `ksh` — makes it a shell script. The interpreter is recognised as a path element or as the argument to `env`. | |
| 32 | + | |
| 33 | +| First line | Result | | |
| 34 | +| --- | --- | | |
| 35 | +| `#!/usr/bin/env golo` | Golo | | |
| 36 | +| `#!/usr/local/bin/golo` | Golo | | |
| 37 | +| `#!/bin/sh` | Shell | | |
| 38 | +| `#!/usr/bin/env bash` | Shell | | |
| 39 | +| `#!/usr/bin/env -S bash -e` | Shell | | |
| 40 | +| `#!/usr/bin/env node` | Not coloured | | |
| 41 | +| Anything not starting `#!` | Not coloured | | |
| 42 | + | |
| 43 | +The order is fixed — extension, then name, then first line — and the first to decide wins. | |
| 44 | + | |
| 45 | +Everything else is shown in plain text. That is not an error — opening a PNG in the editor is not a mistake, it is just not coloured. | |
| 46 | + | |
| 47 | +## Classes | |
| 48 | + | |
| 49 | +Every scanner produces the same vocabulary of classes, and each maps to one theme key. | |
| 50 | + | |
| 51 | +| Class | Theme key | Produced by | | |
| 52 | +| --- | --- | --- | | |
| 53 | +| `identifier` | `syntax.identifier` | Golo, TOML, JavaScript, shell, YAML, Dockerfile | | |
| 54 | +| `keyword` | `syntax.keyword` | Golo, JavaScript, shell, HTML (doctype), XML, Dockerfile | | |
| 55 | +| `type` | `syntax.type` | Golo (capitalised names and module paths), TOML (table headers), YAML (tags) | | |
| 56 | +| `builtin` | `syntax.builtin` | Golo (the interpreter's functions), JavaScript, shell (builtins and expansions), YAML (anchors and aliases), Dockerfile (variables) | | |
| 57 | +| `constant` | `syntax.constant` | Golo, TOML, JavaScript, shell, YAML, HTML and XML (entities) | | |
| 58 | +| `function` | `syntax.function` | Golo, JavaScript, shell (the command) | | |
| 59 | +| `string` | `syntax.string` | all | | |
| 60 | +| `char` | `syntax.char` | Golo (`'c'`) | | |
| 61 | +| `number` | `syntax.number` | Golo, TOML, JavaScript, shell, YAML, Dockerfile | | |
| 62 | +| `comment` | `syntax.comment` | Golo, TOML, JavaScript, shell, HTML, YAML, XML, Dockerfile | | |
| 63 | +| `operator` | `syntax.operator` | Golo, TOML, JavaScript, shell, HTML, YAML (block scalar headers), XML, Dockerfile | | |
| 64 | +| `punctuation` | `syntax.punctuation` | Golo, TOML, JavaScript, shell, Markdown, YAML, Dockerfile | | |
| 65 | +| `heading` | `syntax.heading` | Markdown | | |
| 66 | +| `tag` | `syntax.tag` | HTML, XML | | |
| 67 | +| `attribute` | `syntax.attribute` | HTML, XML, Dockerfile (flags) | | |
| 68 | +| `emphasis` | `syntax.emphasis` | Markdown | | |
| 69 | +| `link` | `syntax.link` | Markdown | | |
| 70 | + | |
| 71 | +Golo produces no `heading`, `tag`, `attribute`, `emphasis` or `link` span. In `turbo-classic`, `syntax.number` and `syntax.constant` are both magenta and `syntax.char` is the green of `syntax.string`, so `42` and `true` share a colour there and so do `'x'` and `"x"`; other themes separate them. See [how to write your own theme](../how-to/write-a-theme.md) if you want to change it. | |
| 72 | + | |
| 73 | +## Golo | |
| 74 | + | |
| 75 | +Hand-written, in `internal/gololang`, against GoloScript's `lexer/lexer.go` and `token/token.go`. What the lexer reads as one token, the scanner colours as one span. | |
| 76 | + | |
| 77 | +**Four constructs cross a line break**, and are carried to the next line exactly as the lexer reads them: a `----` block comment to its closing `----`; a `"…"` string to its closing quote; a `"""…"""` multi-line string to its closing three quotes; a `'…'` character literal to its closing apostrophe. None of the four nests. So an unterminated string colours the rest of the file, until a quote — which is what the interpreter reads it as. | |
| 78 | + | |
| 79 | +| Recognised | As | | |
| 80 | +| --- | --- | | |
| 81 | +| `and`, `augment`, `augmentation`, `await`, `break`, `case`, `catch`, `continue`, `else`, `finally`, `for`, `foreach`, `function`, `if`, `import`, `in`, `is`, `isnt`, `let`, `local`, `match`, `module`, `not`, `oftype`, `or`, `orIfNull`, `otherwise`, `return`, `spawn`, `struct`, `then`, `throw`, `try`, `union`, `var`, `when`, `while`, `with` | keyword | | |
| 82 | +| `true`, `false`, `null` | constant | | |
| 83 | +| the interpreter's 157 builtins — `println`, `print`, `str`, `len`, `list`, `map`, `set`, `array`, `vector`, `range`, `readFile`, `toJSON`, `fromJSON`, `httpGet`, `DynamicObject`, … | builtin | | |
| 84 | +| any other name starting with an ASCII capital — `Point`, `Shape`, `Circle`, `Result_Failure`, `Some` | type | | |
| 85 | +| the dotted path after `module` or `import` — `hello.World`, `gololang.Errors`, `java.util.List` — as one span | type | | |
| 86 | +| the name after `function` — `main` in `function main = \|args\|` | function | | |
| 87 | +| any other lower-case name immediately before `(` | function | | |
| 88 | +| any other name: any Unicode letter or mark, `_`, or an emoji, then letters, digits and the same — `x`, `été`, `名前`, `😀`, `🚀launch` | identifier | | |
| 89 | +| `"…"` with `\n \t \r \\ \" \' \0 \xHH` escapes, across lines | string | | |
| 90 | +| `"""…"""`, across lines, no escapes | string | | |
| 91 | +| `'…'` with the same escapes, across lines | char | | |
| 92 | +| `42`, `3.14`, `1.5e-3`, `2E10`, `42L`, `3.14F`, `2.0f` | number | | |
| 93 | +| `#` to the end of the line, a shebang included | comment | | |
| 94 | +| `----` … `----`, across lines | comment | | |
| 95 | +| `..`, `...` | operator | | |
| 96 | +| runs of `+-*/%=<>!&\|^~?:` — including `->`, `?:`, `==`, `!=`, `<=`, `>=` | operator | | |
| 97 | +| `()[]{},;` and a lone `.` | punctuation | | |
| 98 | +| `$` in `augment Shape$Circle` | punctuation | | |
| 99 | + | |
| 100 | +**A point joins a number only when a digit follows it.** That is the lexer's own test, and it is what keeps `1..3` a number and a range rather than the double `1.` and a stray `.3`. | |
| 101 | + | |
| 102 | +**An exponent may have no digits.** The lexer reads `1e` as a float and leaves the parser to complain, so `1e` is one number span. | |
| 103 | + | |
| 104 | +**A capitalised name is a type by convention, not by rule.** Golo lets you write `let Count = 1`, and it is coloured as a type all the same. Structs, unions, variants and augmentation targets are what people capitalise, and the colour follows the people. | |
| 105 | + | |
| 106 | +**`Some`, `None`, `Ok` and `Err` are types, not constants.** They are variants of ordinary unions declared in `gololang.Errors`, available after an `import`, not builtins. | |
| 107 | + | |
| 108 | +**`DynamicObject` is a builtin, capital and all.** It is in the interpreter's table, and the table wins over the case rule. | |
| 109 | + | |
| 110 | +**Names may hold any Unicode letter, and emoji.** The lexer's `isLetter` admits letters, marks, `_` and four emoji blocks (emoticons, miscellaneous symbols and pictographs, transport and map symbols, supplemental symbols and pictographs); the scanner uses the same rule. | |
| 111 | + | |
| 112 | +**Colouring follows the lexer, not the parser.** GoloScript's parser, as of v0.1.1, refuses several tokens its lexer reads: the `L`, `F` and `f` suffixes, a `'c'` character literal, the `..` range, `orIfNull`, `oftype` and `local function`. They are coloured as the lexer reads them, and the language server marks the line when the parser refuses it. `demos/syntax-tour/lexer-only.golo` holds one of each. | |
| 113 | + | |
| 114 | +**Not recognised**, each for a stated reason: | |
| 115 | + | |
| 116 | +| Not recognised | Because | | |
| 117 | +| --- | --- | | |
| 118 | +| `1_000` as one number | The lexer has no digit separator: `_` starts a name, so this is `1` and then `_000` | | |
| 119 | +| `0xFF`, `0b1010`, `0o17` as numbers | The lexer has no base prefixes: `0xFF` is `0` and then the name `xFF` | | |
| 120 | +| `.5` as a number | The lexer requires a digit before the point, so this is a dot and then `5` | | |
| 121 | +| `42l` as a long | The lexer accepts only the upper-case `L`, so this is `42` and the name `l` | | |
| 122 | +| `---` as a comment | Four dashes open a block comment; three are an operator run | | |
| 123 | +| A keyword after `:` as a method name | `obj: match()` keeps `match` a keyword — the scanner does not track what a colon introduces | | |
| 124 | +| An escape inside `"""…"""` | The lexer appends every rune until the three quotes, so `"""a\"""` ends at its first `"""` | | |
| 125 | +| A constructor as anything but a type | Nothing in the syntax separates `Circle(1.0)` from a type applied to arguments | | |
| 126 | +| An unterminated string stopping at its line | The interpreter reads to the closing quote wherever it is, so the colour follows it — the opposite of Turbo MoonBit's decision, for the opposite reason | | |
| 127 | +| Whether a name is bound in this scope | Nothing here reads more than one line at a time; that is the language server's question, and [F1 answers it](../how-to/ask-about-code.md) | | |
| 128 | + | |
| 129 | +## TOML | |
| 130 | + | |
| 131 | +| Recognised | As | | |
| 132 | +| --- | --- | | |
| 133 | +| `# comment` | comment | | |
| 134 | +| `[table]`, `[[array]]` | the name as a type, the brackets as punctuation | | |
| 135 | +| `key =` | identifier, then operator | | |
| 136 | +| `"basic"`, `'literal'`, `"""multi-line"""`, `'''multi-line'''` | string | | |
| 137 | +| `true`, `false` | constant | | |
| 138 | +| numbers, dates, times, `inf`, `nan` | number | | |
| 139 | + | |
| 140 | +## YAML | |
| 141 | + | |
| 142 | +A compose file, a Kubernetes manifest and a CI workflow are all this: there is no separate dialect, because a dialect would be somebody else's schema to keep in step with. | |
| 143 | + | |
| 144 | +| Recognised | As | | |
| 145 | +| --- | --- | | |
| 146 | +| `# comment` | comment | | |
| 147 | +| `key:` before a space or the end of the line | the key as an identifier, the colon as punctuation | | |
| 148 | +| `"quoted": 1`, `'quoted': 1` | the quoted key as an identifier | | |
| 149 | +| `- ` opening a sequence entry | punctuation | | |
| 150 | +| `"…"`, `'…'` | string | | |
| 151 | +| `true`, `false`, `yes`, `no`, `on`, `off`, `null` | constant, whatever their case | | |
| 152 | +| numbers, dates and times written without quotes | number | | |
| 153 | +| `&anchor`, `*alias` | builtin | | |
| 154 | +| `!!str`, `!Custom` | type | | |
| 155 | +| `---`, `...` | the whole line as punctuation | | |
| 156 | +| `{`, `}`, `[`, `]`, `,` | punctuation | | |
| 157 | +| `\|`, `>`, with their chomping and indentation indicators | the header as an operator, the body as a string | | |
| 158 | + | |
| 159 | +**A colon is a separator only when a space or the end of the line follows it.** `image: nginx:1.27` is a key and one value, and `url: http://example.com/x` is a key and one URL — colouring the inner colons as separators would put every image tag and every URL in three colours. | |
| 160 | + | |
| 161 | +**A block scalar's extent is decided by indentation**, not by a delimiter. The first content line after `|` or `>` fixes the block's indentation; every line indented at least that far belongs to it, and the first line that is not ends it. **A blank line inside a block stays inside it**: a literal scalar keeps its empty lines, and ending the block at the first paragraph break would cut a shell script in a CI file in half. | |
| 162 | + | |
| 163 | +**A `#` needs a space before it to start a comment**, so `colour: ff#00aa` is one scalar. | |
| 164 | + | |
| 165 | +| Not recognised | Because | | |
| 166 | +| --- | --- | | |
| 167 | +| The schema of a compose file, a manifest or a workflow | Colouring `services:` differently from any other key means carrying somebody else's schema, and it goes stale the day they add a key | | |
| 168 | +| Multi-document streams as separate documents | `---` is coloured, but nothing is reset at it; nothing in the colouring depends on document boundaries | | |
| 169 | +| Whether a bare word is a string or a number to a parser | `1.2.3` is a version to a reader and a string to YAML; the scanner colours what it looks like | | |
| 170 | + | |
| 171 | +## Markdown | |
| 172 | + | |
| 173 | +| Recognised | As | | |
| 174 | +| --- | --- | | |
| 175 | +| `# Heading` … `###### Heading` | the whole line as a heading | | |
| 176 | +| `**bold**`, `__bold__`, `*italic*`, `_italic_` | emphasis | | |
| 177 | +| `` `code` `` | string | | |
| 178 | +| `[text](target)`, `` | the whole thing as a link | | |
| 179 | +| `- `, `* `, `+ `, `1. `, `1) ` | the marker as punctuation | | |
| 180 | +| `>` | punctuation | | |
| 181 | +| `---`, `***`, `___` | punctuation | | |
| 182 | +| ` ``` ` and `~~~` fences | the whole block, opening and closing lines included, as a string | | |
| 183 | + | |
| 184 | +A fenced block is **one colour whatever language it announces**: ```` ```golo ```` does not colour its contents as Golo. The run of markers that opens a block must be matched by the same character to close it, so a backtick fence is not closed by a tilde one. An unclosed fence colours to the end of the file. | |
| 185 | + | |
| 186 | +The run of markers opening emphasis must be matched by a run of the same length, so `**bold**` is one span rather than two italics. | |
| 187 | + | |
| 188 | +## JavaScript | |
| 189 | + | |
| 190 | +| Recognised | As | | |
| 191 | +| --- | --- | | |
| 192 | +| `const`, `let`, `function`, `class`, `async`, `await`, `import`, `export`, … | keyword | | |
| 193 | +| `true`, `false`, `null`, `undefined`, `NaN`, `Infinity`, `this` | constant | | |
| 194 | +| `console`, `document`, `window`, `Array`, `Object`, `Promise`, `Math`, `JSON`, … | builtin | | |
| 195 | +| a name immediately before `(` | function | | |
| 196 | +| `"…"`, `'…'` | string | | |
| 197 | +| `` `…` ``, interpolations included, across lines | string | | |
| 198 | +| `//` to end of line, `/* … */` across lines | comment | | |
| 199 | +| `42`, `3.14`, `0x1f`, `0b1010`, `0o777`, `1_000_000`, `1e6`, `10n` | number | | |
| 200 | +| runs of `+-*/%=<>!&|^~?:` | operator | | |
| 201 | +| `()[]{},;.` | punctuation | | |
| 202 | + | |
| 203 | +**Regular-expression literals are not recognised.** Telling `/x/g` from a division needs to know whether the previous token could end an expression; a wrong guess colours the rest of a line as a string, which is worse than leaving a regex the colour of an operator. | |
| 204 | + | |
| 205 | +Globals are recognised by name, so a file that shadows `Math` still has it coloured as a builtin — the same rule Golo's builtins follow here. | |
| 206 | + | |
| 207 | +## HTML | |
| 208 | + | |
| 209 | +| Recognised | As | | |
| 210 | +| --- | --- | | |
| 211 | +| `<tag`, `</tag`, `>`, `/>` | tag | | |
| 212 | +| attribute names, including `data-*`, `xlink:href`, `@click`, `v-bind.prop` | attribute | | |
| 213 | +| `=` | operator | | |
| 214 | +| `"…"`, `'…'` | string | | |
| 215 | +| `<!-- … -->`, across lines | comment | | |
| 216 | +| `&`, `©` | constant | | |
| 217 | +| `<!DOCTYPE …>` and other declarations | keyword | | |
| 218 | + | |
| 219 | +Text between tags is not coloured. A bare `&` with no `;` within 32 characters is left alone, because it is legal text. | |
| 220 | + | |
| 221 | +**The contents of `<script>` and `<style>` are not coloured** as JavaScript and CSS. | |
| 222 | + | |
| 223 | +## XML | |
| 224 | + | |
| 225 | +Its own scanner rather than HTML's, for one reason that matters: CDATA. The whole point of `<![CDATA[ … ]]>` is that its contents are *not* markup, and colouring the tags inside one as tags is exactly backwards. | |
| 226 | + | |
| 227 | +| Recognised | As | | |
| 228 | +| --- | --- | | |
| 229 | +| `<?xml version="1.0"?>` and other processing instructions | the target and `?>` as keyword, the pairs between as attributes and strings | | |
| 230 | +| `<!DOCTYPE …>` and the other `<!` forms | keyword | | |
| 231 | +| `<!-- … -->`, across lines | comment | | |
| 232 | +| `<![CDATA[ … ]]>`, across lines | string | | |
| 233 | +| `<tag`, `</tag`, `>`, `/>` | tag | | |
| 234 | +| `<ns:tag>`, `xsi:type` | the prefix and the local name as **one** span | | |
| 235 | +| attribute names | attribute | | |
| 236 | +| `=` | operator | | |
| 237 | +| `"…"`, `'…'` | string | | |
| 238 | +| `&`, `©` | constant | | |
| 239 | + | |
| 240 | +**A comment and a CDATA section close on different delimiters**, and are carried separately: a `-->` inside a CDATA section does not end it. | |
| 241 | + | |
| 242 | +**A bare `&` with no semicolon within 32 characters is left alone**, because it is legal text in plenty of documents and swallowing the rest of the line would be the bigger mistake. | |
| 243 | + | |
| 244 | +Text between tags is not coloured. | |
| 245 | + | |
| 246 | +## Shell | |
| 247 | + | |
| 248 | +Applies to `sh`, `bash` and `zsh` alike: the keywords recognised are the ones they share. | |
| 249 | + | |
| 250 | +| Recognised | As | | |
| 251 | +| --- | --- | | |
| 252 | +| `if`, `then`, `fi`, `for`, `while`, `case`, `esac`, `function`, `return`, … | keyword | | |
| 253 | +| `true`, `false` | constant | | |
| 254 | +| `echo`, `printf`, `export`, `local`, `read`, `cd`, `set`, `source`, … | builtin | | |
| 255 | +| `$NAME`, `${…}`, `$(…)`, `$1`, `$?`, `$@` | builtin | | |
| 256 | +| the **first bare word on a line** | function | | |
| 257 | +| every later bare word, and `NAME` in `NAME=value` | identifier | | |
| 258 | +| `'…'`, with nothing escaped or expanded inside | string | | |
| 259 | +| `"…"`, with the expansions inside it coloured as expansions | string | | |
| 260 | +| `#` to end of line | comment | | |
| 261 | + | |
| 262 | +`$(a $(b) c)` is one span: nesting is counted. An option such as `-euo` is one word, not a minus and a word. | |
| 263 | + | |
| 264 | +**Heredocs are not recognised.** `<<EOF` and the text after it are coloured as ordinary shell. | |
| 265 | + | |
| 266 | +## Dockerfile | |
| 267 | + | |
| 268 | +| Recognised | As | | |
| 269 | +| --- | --- | | |
| 270 | +| `FROM`, `RUN`, `COPY`, `ADD`, `ARG`, `ENV`, `CMD`, `ENTRYPOINT`, `EXPOSE`, `LABEL`, `USER`, `VOLUME`, `WORKDIR`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `MAINTAINER` | keyword, in any case | | |
| 271 | +| `AS`, `NONE` | keyword | | |
| 272 | +| `# comment`, including the `# syntax=` and `# escape=` directives | comment | | |
| 273 | +| `--from=builder`, `--chown=me:me` | the flag name as an attribute | | |
| 274 | +| `$NAME`, `${NAME}`, `${NAME:-default}` | builtin, as one span to the closing brace | | |
| 275 | +| `"…"`, `'…'` | string | | |
| 276 | +| a trailing `\` | operator | | |
| 277 | +| numbers | number | | |
| 278 | +| paths and image references — `/usr/local/bin`, `golang:1.26-alpine` | identifier, as **one** span | | |
| 279 | + | |
| 280 | +**Only the first word of a line can be an instruction**, and a word that is not one is an argument — which is what keeps a continuation line's first word out of the keyword colour. | |
| 281 | + | |
| 282 | +**Nothing crosses a line break.** A `\` joins two lines for Docker, but each half still reads as a command and is coloured on its own. | |
| 283 | + | |
| 284 | +| Not recognised | Because | | |
| 285 | +| --- | --- | | |
| 286 | +| The shell inside a `RUN` | It would mean running the shell scanner over part of a line and mapping its columns out, and `RUN` may hold any language | | |
| 287 | +| Heredocs in a `RUN` | The same reason the shell scanner does not recognise them | | |
| 288 | +| Which stage a `--from` names | Nothing here reads the rest of the file | | |
| 289 | + | |
| 290 | +## See also | |
| 291 | + | |
| 292 | +- [Theme file format](themes.md) — every key these classes resolve to | |
| 293 | +- [Colouring and completion](../explanation/colouring-and-completion.md) — why the scanners are written this way | |
| 294 | +- [How to write your own theme](../how-to/write-a-theme.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,294 @@ | |||
| 1 | +# Reference: languages coloured | ||
| 2 | + | ||
| 3 | +> Neutral description of which files Turbo Golo colours, how it decides, and what each scanner recognises. | ||
| 4 | + | ||
| 5 | +## Recognition | ||
| 6 | + | ||
| 7 | +A file's **extension** decides whenever it is one of these: | ||
| 8 | + | ||
| 9 | +| Extension | Language | | ||
| 10 | +| --- | --- | | ||
| 11 | +| `.golo` | Golo | | ||
| 12 | +| `.toml` | TOML | | ||
| 13 | +| `.yaml`, `.yml` | YAML | | ||
| 14 | +| `.md`, `.markdown` | Markdown | | ||
| 15 | +| `.js`, `.mjs`, `.cjs` | JavaScript | | ||
| 16 | +| `.html`, `.htm` | HTML | | ||
| 17 | +| `.xml`, `.xsd`, `.xsl`, `.xslt`, `.svg`, `.plist`, `.csproj`, `.pom` | XML | | ||
| 18 | +| `.sh`, `.bash`, `.zsh` | Shell | | ||
| 19 | +| `.dockerfile`, `.containerfile` | Dockerfile | | ||
| 20 | + | ||
| 21 | +Extensions are matched case-insensitively, and only the last one counts: `notes.golo.md` is Markdown, and `main.golo.backup` is not Golo. | ||
| 22 | + | ||
| 23 | +A file whose extension decides nothing is looked up by **name** next. Only files that carry no useful extension need this: | ||
| 24 | + | ||
| 25 | +| Name | Language | | ||
| 26 | +| --- | --- | | ||
| 27 | +| `Dockerfile`, `Containerfile` | Dockerfile | | ||
| 28 | + | ||
| 29 | +A name matches on the whole of it or on the part before the first dot, ignoring case — so `Dockerfile`, `dockerfile` and `Dockerfile.dev` are all recognised, while `Dockerfile.md` is Markdown, because the extension is consulted first. | ||
| 30 | + | ||
| 31 | +A file that neither table claims is read by its **first line**. A shebang naming `golo` makes it Golo: `#` opens a comment in Golo, so the interpreter reads the line as one, and a script installed without its extension and run as a command is Golo and nothing else. A shebang naming a shell — `sh`, `bash`, `zsh`, `dash` or `ksh` — makes it a shell script. The interpreter is recognised as a path element or as the argument to `env`. | ||
| 32 | + | ||
| 33 | +| First line | Result | | ||
| 34 | +| --- | --- | | ||
| 35 | +| `#!/usr/bin/env golo` | Golo | | ||
| 36 | +| `#!/usr/local/bin/golo` | Golo | | ||
| 37 | +| `#!/bin/sh` | Shell | | ||
| 38 | +| `#!/usr/bin/env bash` | Shell | | ||
| 39 | +| `#!/usr/bin/env -S bash -e` | Shell | | ||
| 40 | +| `#!/usr/bin/env node` | Not coloured | | ||
| 41 | +| Anything not starting `#!` | Not coloured | | ||
| 42 | + | ||
| 43 | +The order is fixed — extension, then name, then first line — and the first to decide wins. | ||
| 44 | + | ||
| 45 | +Everything else is shown in plain text. That is not an error — opening a PNG in the editor is not a mistake, it is just not coloured. | ||
| 46 | + | ||
| 47 | +## Classes | ||
| 48 | + | ||
| 49 | +Every scanner produces the same vocabulary of classes, and each maps to one theme key. | ||
| 50 | + | ||
| 51 | +| Class | Theme key | Produced by | | ||
| 52 | +| --- | --- | --- | | ||
| 53 | +| `identifier` | `syntax.identifier` | Golo, TOML, JavaScript, shell, YAML, Dockerfile | | ||
| 54 | +| `keyword` | `syntax.keyword` | Golo, JavaScript, shell, HTML (doctype), XML, Dockerfile | | ||
| 55 | +| `type` | `syntax.type` | Golo (capitalised names and module paths), TOML (table headers), YAML (tags) | | ||
| 56 | +| `builtin` | `syntax.builtin` | Golo (the interpreter's functions), JavaScript, shell (builtins and expansions), YAML (anchors and aliases), Dockerfile (variables) | | ||
| 57 | +| `constant` | `syntax.constant` | Golo, TOML, JavaScript, shell, YAML, HTML and XML (entities) | | ||
| 58 | +| `function` | `syntax.function` | Golo, JavaScript, shell (the command) | | ||
| 59 | +| `string` | `syntax.string` | all | | ||
| 60 | +| `char` | `syntax.char` | Golo (`'c'`) | | ||
| 61 | +| `number` | `syntax.number` | Golo, TOML, JavaScript, shell, YAML, Dockerfile | | ||
| 62 | +| `comment` | `syntax.comment` | Golo, TOML, JavaScript, shell, HTML, YAML, XML, Dockerfile | | ||
| 63 | +| `operator` | `syntax.operator` | Golo, TOML, JavaScript, shell, HTML, YAML (block scalar headers), XML, Dockerfile | | ||
| 64 | +| `punctuation` | `syntax.punctuation` | Golo, TOML, JavaScript, shell, Markdown, YAML, Dockerfile | | ||
| 65 | +| `heading` | `syntax.heading` | Markdown | | ||
| 66 | +| `tag` | `syntax.tag` | HTML, XML | | ||
| 67 | +| `attribute` | `syntax.attribute` | HTML, XML, Dockerfile (flags) | | ||
| 68 | +| `emphasis` | `syntax.emphasis` | Markdown | | ||
| 69 | +| `link` | `syntax.link` | Markdown | | ||
| 70 | + | ||
| 71 | +Golo produces no `heading`, `tag`, `attribute`, `emphasis` or `link` span. In `turbo-classic`, `syntax.number` and `syntax.constant` are both magenta and `syntax.char` is the green of `syntax.string`, so `42` and `true` share a colour there and so do `'x'` and `"x"`; other themes separate them. See [how to write your own theme](../how-to/write-a-theme.md) if you want to change it. | ||
| 72 | + | ||
| 73 | +## Golo | ||
| 74 | + | ||
| 75 | +Hand-written, in `internal/gololang`, against GoloScript's `lexer/lexer.go` and `token/token.go`. What the lexer reads as one token, the scanner colours as one span. | ||
| 76 | + | ||
| 77 | +**Four constructs cross a line break**, and are carried to the next line exactly as the lexer reads them: a `----` block comment to its closing `----`; a `"…"` string to its closing quote; a `"""…"""` multi-line string to its closing three quotes; a `'…'` character literal to its closing apostrophe. None of the four nests. So an unterminated string colours the rest of the file, until a quote — which is what the interpreter reads it as. | ||
| 78 | + | ||
| 79 | +| Recognised | As | | ||
| 80 | +| --- | --- | | ||
| 81 | +| `and`, `augment`, `augmentation`, `await`, `break`, `case`, `catch`, `continue`, `else`, `finally`, `for`, `foreach`, `function`, `if`, `import`, `in`, `is`, `isnt`, `let`, `local`, `match`, `module`, `not`, `oftype`, `or`, `orIfNull`, `otherwise`, `return`, `spawn`, `struct`, `then`, `throw`, `try`, `union`, `var`, `when`, `while`, `with` | keyword | | ||
| 82 | +| `true`, `false`, `null` | constant | | ||
| 83 | +| the interpreter's 157 builtins — `println`, `print`, `str`, `len`, `list`, `map`, `set`, `array`, `vector`, `range`, `readFile`, `toJSON`, `fromJSON`, `httpGet`, `DynamicObject`, … | builtin | | ||
| 84 | +| any other name starting with an ASCII capital — `Point`, `Shape`, `Circle`, `Result_Failure`, `Some` | type | | ||
| 85 | +| the dotted path after `module` or `import` — `hello.World`, `gololang.Errors`, `java.util.List` — as one span | type | | ||
| 86 | +| the name after `function` — `main` in `function main = \|args\|` | function | | ||
| 87 | +| any other lower-case name immediately before `(` | function | | ||
| 88 | +| any other name: any Unicode letter or mark, `_`, or an emoji, then letters, digits and the same — `x`, `été`, `名前`, `😀`, `🚀launch` | identifier | | ||
| 89 | +| `"…"` with `\n \t \r \\ \" \' \0 \xHH` escapes, across lines | string | | ||
| 90 | +| `"""…"""`, across lines, no escapes | string | | ||
| 91 | +| `'…'` with the same escapes, across lines | char | | ||
| 92 | +| `42`, `3.14`, `1.5e-3`, `2E10`, `42L`, `3.14F`, `2.0f` | number | | ||
| 93 | +| `#` to the end of the line, a shebang included | comment | | ||
| 94 | +| `----` … `----`, across lines | comment | | ||
| 95 | +| `..`, `...` | operator | | ||
| 96 | +| runs of `+-*/%=<>!&\|^~?:` — including `->`, `?:`, `==`, `!=`, `<=`, `>=` | operator | | ||
| 97 | +| `()[]{},;` and a lone `.` | punctuation | | ||
| 98 | +| `$` in `augment Shape$Circle` | punctuation | | ||
| 99 | + | ||
| 100 | +**A point joins a number only when a digit follows it.** That is the lexer's own test, and it is what keeps `1..3` a number and a range rather than the double `1.` and a stray `.3`. | ||
| 101 | + | ||
| 102 | +**An exponent may have no digits.** The lexer reads `1e` as a float and leaves the parser to complain, so `1e` is one number span. | ||
| 103 | + | ||
| 104 | +**A capitalised name is a type by convention, not by rule.** Golo lets you write `let Count = 1`, and it is coloured as a type all the same. Structs, unions, variants and augmentation targets are what people capitalise, and the colour follows the people. | ||
| 105 | + | ||
| 106 | +**`Some`, `None`, `Ok` and `Err` are types, not constants.** They are variants of ordinary unions declared in `gololang.Errors`, available after an `import`, not builtins. | ||
| 107 | + | ||
| 108 | +**`DynamicObject` is a builtin, capital and all.** It is in the interpreter's table, and the table wins over the case rule. | ||
| 109 | + | ||
| 110 | +**Names may hold any Unicode letter, and emoji.** The lexer's `isLetter` admits letters, marks, `_` and four emoji blocks (emoticons, miscellaneous symbols and pictographs, transport and map symbols, supplemental symbols and pictographs); the scanner uses the same rule. | ||
| 111 | + | ||
| 112 | +**Colouring follows the lexer, not the parser.** GoloScript's parser, as of v0.1.1, refuses several tokens its lexer reads: the `L`, `F` and `f` suffixes, a `'c'` character literal, the `..` range, `orIfNull`, `oftype` and `local function`. They are coloured as the lexer reads them, and the language server marks the line when the parser refuses it. `demos/syntax-tour/lexer-only.golo` holds one of each. | ||
| 113 | + | ||
| 114 | +**Not recognised**, each for a stated reason: | ||
| 115 | + | ||
| 116 | +| Not recognised | Because | | ||
| 117 | +| --- | --- | | ||
| 118 | +| `1_000` as one number | The lexer has no digit separator: `_` starts a name, so this is `1` and then `_000` | | ||
| 119 | +| `0xFF`, `0b1010`, `0o17` as numbers | The lexer has no base prefixes: `0xFF` is `0` and then the name `xFF` | | ||
| 120 | +| `.5` as a number | The lexer requires a digit before the point, so this is a dot and then `5` | | ||
| 121 | +| `42l` as a long | The lexer accepts only the upper-case `L`, so this is `42` and the name `l` | | ||
| 122 | +| `---` as a comment | Four dashes open a block comment; three are an operator run | | ||
| 123 | +| A keyword after `:` as a method name | `obj: match()` keeps `match` a keyword — the scanner does not track what a colon introduces | | ||
| 124 | +| An escape inside `"""…"""` | The lexer appends every rune until the three quotes, so `"""a\"""` ends at its first `"""` | | ||
| 125 | +| A constructor as anything but a type | Nothing in the syntax separates `Circle(1.0)` from a type applied to arguments | | ||
| 126 | +| An unterminated string stopping at its line | The interpreter reads to the closing quote wherever it is, so the colour follows it — the opposite of Turbo MoonBit's decision, for the opposite reason | | ||
| 127 | +| Whether a name is bound in this scope | Nothing here reads more than one line at a time; that is the language server's question, and [F1 answers it](../how-to/ask-about-code.md) | | ||
| 128 | + | ||
| 129 | +## TOML | ||
| 130 | + | ||
| 131 | +| Recognised | As | | ||
| 132 | +| --- | --- | | ||
| 133 | +| `# comment` | comment | | ||
| 134 | +| `[table]`, `[[array]]` | the name as a type, the brackets as punctuation | | ||
| 135 | +| `key =` | identifier, then operator | | ||
| 136 | +| `"basic"`, `'literal'`, `"""multi-line"""`, `'''multi-line'''` | string | | ||
| 137 | +| `true`, `false` | constant | | ||
| 138 | +| numbers, dates, times, `inf`, `nan` | number | | ||
| 139 | + | ||
| 140 | +## YAML | ||
| 141 | + | ||
| 142 | +A compose file, a Kubernetes manifest and a CI workflow are all this: there is no separate dialect, because a dialect would be somebody else's schema to keep in step with. | ||
| 143 | + | ||
| 144 | +| Recognised | As | | ||
| 145 | +| --- | --- | | ||
| 146 | +| `# comment` | comment | | ||
| 147 | +| `key:` before a space or the end of the line | the key as an identifier, the colon as punctuation | | ||
| 148 | +| `"quoted": 1`, `'quoted': 1` | the quoted key as an identifier | | ||
| 149 | +| `- ` opening a sequence entry | punctuation | | ||
| 150 | +| `"…"`, `'…'` | string | | ||
| 151 | +| `true`, `false`, `yes`, `no`, `on`, `off`, `null` | constant, whatever their case | | ||
| 152 | +| numbers, dates and times written without quotes | number | | ||
| 153 | +| `&anchor`, `*alias` | builtin | | ||
| 154 | +| `!!str`, `!Custom` | type | | ||
| 155 | +| `---`, `...` | the whole line as punctuation | | ||
| 156 | +| `{`, `}`, `[`, `]`, `,` | punctuation | | ||
| 157 | +| `\|`, `>`, with their chomping and indentation indicators | the header as an operator, the body as a string | | ||
| 158 | + | ||
| 159 | +**A colon is a separator only when a space or the end of the line follows it.** `image: nginx:1.27` is a key and one value, and `url: http://example.com/x` is a key and one URL — colouring the inner colons as separators would put every image tag and every URL in three colours. | ||
| 160 | + | ||
| 161 | +**A block scalar's extent is decided by indentation**, not by a delimiter. The first content line after `|` or `>` fixes the block's indentation; every line indented at least that far belongs to it, and the first line that is not ends it. **A blank line inside a block stays inside it**: a literal scalar keeps its empty lines, and ending the block at the first paragraph break would cut a shell script in a CI file in half. | ||
| 162 | + | ||
| 163 | +**A `#` needs a space before it to start a comment**, so `colour: ff#00aa` is one scalar. | ||
| 164 | + | ||
| 165 | +| Not recognised | Because | | ||
| 166 | +| --- | --- | | ||
| 167 | +| The schema of a compose file, a manifest or a workflow | Colouring `services:` differently from any other key means carrying somebody else's schema, and it goes stale the day they add a key | | ||
| 168 | +| Multi-document streams as separate documents | `---` is coloured, but nothing is reset at it; nothing in the colouring depends on document boundaries | | ||
| 169 | +| Whether a bare word is a string or a number to a parser | `1.2.3` is a version to a reader and a string to YAML; the scanner colours what it looks like | | ||
| 170 | + | ||
| 171 | +## Markdown | ||
| 172 | + | ||
| 173 | +| Recognised | As | | ||
| 174 | +| --- | --- | | ||
| 175 | +| `# Heading` … `###### Heading` | the whole line as a heading | | ||
| 176 | +| `**bold**`, `__bold__`, `*italic*`, `_italic_` | emphasis | | ||
| 177 | +| `` `code` `` | string | | ||
| 178 | +| `[text](target)`, `` | the whole thing as a link | | ||
| 179 | +| `- `, `* `, `+ `, `1. `, `1) ` | the marker as punctuation | | ||
| 180 | +| `>` | punctuation | | ||
| 181 | +| `---`, `***`, `___` | punctuation | | ||
| 182 | +| ` ``` ` and `~~~` fences | the whole block, opening and closing lines included, as a string | | ||
| 183 | + | ||
| 184 | +A fenced block is **one colour whatever language it announces**: ```` ```golo ```` does not colour its contents as Golo. The run of markers that opens a block must be matched by the same character to close it, so a backtick fence is not closed by a tilde one. An unclosed fence colours to the end of the file. | ||
| 185 | + | ||
| 186 | +The run of markers opening emphasis must be matched by a run of the same length, so `**bold**` is one span rather than two italics. | ||
| 187 | + | ||
| 188 | +## JavaScript | ||
| 189 | + | ||
| 190 | +| Recognised | As | | ||
| 191 | +| --- | --- | | ||
| 192 | +| `const`, `let`, `function`, `class`, `async`, `await`, `import`, `export`, … | keyword | | ||
| 193 | +| `true`, `false`, `null`, `undefined`, `NaN`, `Infinity`, `this` | constant | | ||
| 194 | +| `console`, `document`, `window`, `Array`, `Object`, `Promise`, `Math`, `JSON`, … | builtin | | ||
| 195 | +| a name immediately before `(` | function | | ||
| 196 | +| `"…"`, `'…'` | string | | ||
| 197 | +| `` `…` ``, interpolations included, across lines | string | | ||
| 198 | +| `//` to end of line, `/* … */` across lines | comment | | ||
| 199 | +| `42`, `3.14`, `0x1f`, `0b1010`, `0o777`, `1_000_000`, `1e6`, `10n` | number | | ||
| 200 | +| runs of `+-*/%=<>!&|^~?:` | operator | | ||
| 201 | +| `()[]{},;.` | punctuation | | ||
| 202 | + | ||
| 203 | +**Regular-expression literals are not recognised.** Telling `/x/g` from a division needs to know whether the previous token could end an expression; a wrong guess colours the rest of a line as a string, which is worse than leaving a regex the colour of an operator. | ||
| 204 | + | ||
| 205 | +Globals are recognised by name, so a file that shadows `Math` still has it coloured as a builtin — the same rule Golo's builtins follow here. | ||
| 206 | + | ||
| 207 | +## HTML | ||
| 208 | + | ||
| 209 | +| Recognised | As | | ||
| 210 | +| --- | --- | | ||
| 211 | +| `<tag`, `</tag`, `>`, `/>` | tag | | ||
| 212 | +| attribute names, including `data-*`, `xlink:href`, `@click`, `v-bind.prop` | attribute | | ||
| 213 | +| `=` | operator | | ||
| 214 | +| `"…"`, `'…'` | string | | ||
| 215 | +| `<!-- … -->`, across lines | comment | | ||
| 216 | +| `&`, `©` | constant | | ||
| 217 | +| `<!DOCTYPE …>` and other declarations | keyword | | ||
| 218 | + | ||
| 219 | +Text between tags is not coloured. A bare `&` with no `;` within 32 characters is left alone, because it is legal text. | ||
| 220 | + | ||
| 221 | +**The contents of `<script>` and `<style>` are not coloured** as JavaScript and CSS. | ||
| 222 | + | ||
| 223 | +## XML | ||
| 224 | + | ||
| 225 | +Its own scanner rather than HTML's, for one reason that matters: CDATA. The whole point of `<![CDATA[ … ]]>` is that its contents are *not* markup, and colouring the tags inside one as tags is exactly backwards. | ||
| 226 | + | ||
| 227 | +| Recognised | As | | ||
| 228 | +| --- | --- | | ||
| 229 | +| `<?xml version="1.0"?>` and other processing instructions | the target and `?>` as keyword, the pairs between as attributes and strings | | ||
| 230 | +| `<!DOCTYPE …>` and the other `<!` forms | keyword | | ||
| 231 | +| `<!-- … -->`, across lines | comment | | ||
| 232 | +| `<![CDATA[ … ]]>`, across lines | string | | ||
| 233 | +| `<tag`, `</tag`, `>`, `/>` | tag | | ||
| 234 | +| `<ns:tag>`, `xsi:type` | the prefix and the local name as **one** span | | ||
| 235 | +| attribute names | attribute | | ||
| 236 | +| `=` | operator | | ||
| 237 | +| `"…"`, `'…'` | string | | ||
| 238 | +| `&`, `©` | constant | | ||
| 239 | + | ||
| 240 | +**A comment and a CDATA section close on different delimiters**, and are carried separately: a `-->` inside a CDATA section does not end it. | ||
| 241 | + | ||
| 242 | +**A bare `&` with no semicolon within 32 characters is left alone**, because it is legal text in plenty of documents and swallowing the rest of the line would be the bigger mistake. | ||
| 243 | + | ||
| 244 | +Text between tags is not coloured. | ||
| 245 | + | ||
| 246 | +## Shell | ||
| 247 | + | ||
| 248 | +Applies to `sh`, `bash` and `zsh` alike: the keywords recognised are the ones they share. | ||
| 249 | + | ||
| 250 | +| Recognised | As | | ||
| 251 | +| --- | --- | | ||
| 252 | +| `if`, `then`, `fi`, `for`, `while`, `case`, `esac`, `function`, `return`, … | keyword | | ||
| 253 | +| `true`, `false` | constant | | ||
| 254 | +| `echo`, `printf`, `export`, `local`, `read`, `cd`, `set`, `source`, … | builtin | | ||
| 255 | +| `$NAME`, `${…}`, `$(…)`, `$1`, `$?`, `$@` | builtin | | ||
| 256 | +| the **first bare word on a line** | function | | ||
| 257 | +| every later bare word, and `NAME` in `NAME=value` | identifier | | ||
| 258 | +| `'…'`, with nothing escaped or expanded inside | string | | ||
| 259 | +| `"…"`, with the expansions inside it coloured as expansions | string | | ||
| 260 | +| `#` to end of line | comment | | ||
| 261 | + | ||
| 262 | +`$(a $(b) c)` is one span: nesting is counted. An option such as `-euo` is one word, not a minus and a word. | ||
| 263 | + | ||
| 264 | +**Heredocs are not recognised.** `<<EOF` and the text after it are coloured as ordinary shell. | ||
| 265 | + | ||
| 266 | +## Dockerfile | ||
| 267 | + | ||
| 268 | +| Recognised | As | | ||
| 269 | +| --- | --- | | ||
| 270 | +| `FROM`, `RUN`, `COPY`, `ADD`, `ARG`, `ENV`, `CMD`, `ENTRYPOINT`, `EXPOSE`, `LABEL`, `USER`, `VOLUME`, `WORKDIR`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `MAINTAINER` | keyword, in any case | | ||
| 271 | +| `AS`, `NONE` | keyword | | ||
| 272 | +| `# comment`, including the `# syntax=` and `# escape=` directives | comment | | ||
| 273 | +| `--from=builder`, `--chown=me:me` | the flag name as an attribute | | ||
| 274 | +| `$NAME`, `${NAME}`, `${NAME:-default}` | builtin, as one span to the closing brace | | ||
| 275 | +| `"…"`, `'…'` | string | | ||
| 276 | +| a trailing `\` | operator | | ||
| 277 | +| numbers | number | | ||
| 278 | +| paths and image references — `/usr/local/bin`, `golang:1.26-alpine` | identifier, as **one** span | | ||
| 279 | + | ||
| 280 | +**Only the first word of a line can be an instruction**, and a word that is not one is an argument — which is what keeps a continuation line's first word out of the keyword colour. | ||
| 281 | + | ||
| 282 | +**Nothing crosses a line break.** A `\` joins two lines for Docker, but each half still reads as a command and is coloured on its own. | ||
| 283 | + | ||
| 284 | +| Not recognised | Because | | ||
| 285 | +| --- | --- | | ||
| 286 | +| The shell inside a `RUN` | It would mean running the shell scanner over part of a line and mapping its columns out, and `RUN` may hold any language | | ||
| 287 | +| Heredocs in a `RUN` | The same reason the shell scanner does not recognise them | | ||
| 288 | +| Which stage a `--from` names | Nothing here reads the rest of the file | | ||
| 289 | + | ||
| 290 | +## See also | ||
| 291 | + | ||
| 292 | +- [Theme file format](themes.md) — every key these classes resolve to | ||
| 293 | +- [Colouring and completion](../explanation/colouring-and-completion.md) — why the scanners are written this way | ||
| 294 | +- [How to write your own theme](../how-to/write-a-theme.md) | ||
added
docs/en/reference/project-settings.md +111 -0 | new file mode 100644 | ||
| @@ -0,0 +1,111 @@ | ||
| 1 | +# Reference: project settings | |
| 2 | + | |
| 3 | +> Neutral description of `.turbo-golo/settings.toml`: where it is looked for, what it may contain, and what writes to it. | |
| 4 | + | |
| 5 | +## Location | |
| 6 | + | |
| 7 | +| Property | Value | | |
| 8 | +| --- | --- | | |
| 9 | +| Directory | `.turbo-golo` in the editor's working directory | | |
| 10 | +| File | `.turbo-golo/settings.toml` | | |
| 11 | +| Search | The working directory only. Parent directories are **not** searched. | | |
| 12 | +| Read | When the editor starts, and again whenever the file is saved from inside the editor | | |
| 13 | +| Required | No. A project without one gets the defaults below. | | |
| 14 | + | |
| 15 | +## Keys | |
| 16 | + | |
| 17 | +Every key is optional, and every key lives in the `[editor]` table. A key that is absent keeps its default; a key present with any value overrides it, including a value equal to the default. | |
| 18 | + | |
| 19 | +| Key | Type | Default | Description | | |
| 20 | +| --- | --- | --- | --- | | |
| 21 | +| `theme` | string | the editor's own default (`turbo-classic`) | Name of the colour theme to start in, as listed by `turbo-golo -list-themes` | | |
| 22 | +| `autosave` | boolean | `false` | Whether modified files are written without being asked. The file **Create project settings** writes sets it to `true`; the default here is what applies to a project with no settings file at all. | | |
| 23 | +| `autosave_delay` | string | `"2s"` | How long after the last keystroke to wait. A Go duration: `"500ms"`, `"2s"`, `"1m"`. Only consulted when `autosave` is true. | | |
| 24 | + | |
| 25 | +### Example | |
| 26 | + | |
| 27 | +```toml | |
| 28 | +[editor] | |
| 29 | +theme = "turbo-dark" | |
| 30 | +autosave = true | |
| 31 | +autosave_delay = "500ms" | |
| 32 | +``` | |
| 33 | + | |
| 34 | +## Theme precedence | |
| 35 | + | |
| 36 | +Highest first: | |
| 37 | + | |
| 38 | +| Source | Wins over | | |
| 39 | +| --- | --- | | |
| 40 | +| `-theme` on the command line | everything | | |
| 41 | +| `theme` in the settings file | the built-in default | | |
| 42 | +| The built-in default `turbo-classic` | — | | |
| 43 | + | |
| 44 | +An unknown theme name at any level falls back to the built-in default rather than failing. | |
| 45 | + | |
| 46 | +## When a change takes effect | |
| 47 | + | |
| 48 | +The file is read at start-up, and **again every time it is saved from inside the editor** — so a change made in the editor is in force the moment you press `F2`, with no restart. | |
| 49 | + | |
| 50 | +| Key | Re-applied on save | Why | | |
| 51 | +| --- | --- | --- | | |
| 52 | +| `autosave` | yes | | | |
| 53 | +| `autosave_delay` | yes | | | |
| 54 | +| `theme` | **no** | Options ▸ Theme is the live way to change it, and already writes the choice back here. A `-theme` flag given on the command line is the more explicit statement for that session and is not overridden by a file being saved. | | |
| 55 | + | |
| 56 | +| Outcome | Status bar | | |
| 57 | +| --- | --- | | |
| 58 | +| Read and applied | `Applied .turbo-golo/settings.toml — autosave on (2s)` | | |
| 59 | +| Read and applied, autosave off | `Applied .turbo-golo/settings.toml — autosave off` | | |
| 60 | +| Saved, but no longer valid TOML | `Saved, but not applied: …` — the previous values stay in force | | |
| 61 | + | |
| 62 | +Saving is saving, whoever did it: automatic saving writing the settings file re-applies them exactly as `F2` does. Editing the file **outside** the editor is not noticed; nothing watches it. | |
| 63 | + | |
| 64 | +## Automatic saving | |
| 65 | + | |
| 66 | +| Behaviour | Detail | | |
| 67 | +| --- | --- | | |
| 68 | +| Trigger | The delay elapsing with no edit in any window | | |
| 69 | +| Scope | Every open file with a name, not only the front one | | |
| 70 | +| Deadline | One for the whole editor, restarted by any edit in any window | | |
| 71 | +| Files with no name | Never saved; never asked about | | |
| 72 | +| Report | `Saved <name>` on the status bar | | |
| 73 | +| Failure | Reported on the status bar, never in a dialog, and not retried until the next edit | | |
| 74 | +| Closing a window | Saves instead of asking, when the file has a name | | |
| 75 | +| Leaving the editor | Saves instead of asking, when the file has a name | | |
| 76 | + | |
| 77 | +## Writes | |
| 78 | + | |
| 79 | +The settings file is written by exactly two actions. Nothing else in the editor writes to it, and nothing creates it by itself. | |
| 80 | + | |
| 81 | +| Action | Effect | | |
| 82 | +| --- | --- | | |
| 83 | +| **Options ▸ Create project settings** | Creates `.turbo-golo/settings.toml` with the theme in use, `autosave = true`, `autosave_delay = "2s"`, and explanatory comments. Greyed out once the project has one, so it cannot be chosen twice. | | |
| 84 | +| **Options ▸ Theme** | Rewrites the `theme` value **only when the file already exists**. Comments, blank lines, key order and any trailing comment on the theme line are kept. | | |
| 85 | + | |
| 86 | +Both write through a temporary file in the same directory, renamed into place, so an interrupted write leaves the previous file intact. | |
| 87 | + | |
| 88 | +## Menu items | |
| 89 | + | |
| 90 | +| Item | Menu | Needs a file | Effect | | |
| 91 | +| --- | --- | --- | --- | | |
| 92 | +| Create project settings | Options | refuses the file | As above, then opens the file. Greyed out once the project has one. | | |
| 93 | +| Project settings… | Options | requires the file | Opens `.turbo-golo/settings.toml`. Greyed out until the project has one. | | |
| 94 | + | |
| 95 | +## Errors | |
| 96 | + | |
| 97 | +| Message | Cause | | |
| 98 | +| --- | --- | | |
| 99 | +| `turbo-golo: reading …/settings.toml: …` on standard error | The file is present but is not valid TOML. The editor opens with its defaults. | | |
| 100 | +| `reading …: autosave_delay "x" is not a duration such as "2s"` | `autosave_delay` is not a Go duration | | |
| 101 | +| `reading …: autosave_delay must be positive, not "0s"` | `autosave_delay` is zero or negative | | |
| 102 | +| `Already there: .turbo-golo/settings.toml` on the status bar | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. | | |
| 103 | +| `Saved, but not applied: …` on the status bar | The settings file was written but no longer parses. The previous values stay in force. | | |
| 104 | +| `This project has no .turbo-golo/settings.toml yet.` | **Project settings…** in a project that has none | | |
| 105 | +| `Theme set for this session only: …` | The theme changed but the settings file could not be written | | |
| 106 | + | |
| 107 | +## See also | |
| 108 | + | |
| 109 | +- [How to give a project its own settings](../how-to/configure-a-project.md) | |
| 110 | +- [Project settings](../explanation/project-settings.md) | |
| 111 | +- [Theme file format](themes.md) — a different file, in the same language | |
| new file mode 100644 | |||
| @@ -0,0 +1,111 @@ | |||
| 1 | +# Reference: project settings | ||
| 2 | + | ||
| 3 | +> Neutral description of `.turbo-golo/settings.toml`: where it is looked for, what it may contain, and what writes to it. | ||
| 4 | + | ||
| 5 | +## Location | ||
| 6 | + | ||
| 7 | +| Property | Value | | ||
| 8 | +| --- | --- | | ||
| 9 | +| Directory | `.turbo-golo` in the editor's working directory | | ||
| 10 | +| File | `.turbo-golo/settings.toml` | | ||
| 11 | +| Search | The working directory only. Parent directories are **not** searched. | | ||
| 12 | +| Read | When the editor starts, and again whenever the file is saved from inside the editor | | ||
| 13 | +| Required | No. A project without one gets the defaults below. | | ||
| 14 | + | ||
| 15 | +## Keys | ||
| 16 | + | ||
| 17 | +Every key is optional, and every key lives in the `[editor]` table. A key that is absent keeps its default; a key present with any value overrides it, including a value equal to the default. | ||
| 18 | + | ||
| 19 | +| Key | Type | Default | Description | | ||
| 20 | +| --- | --- | --- | --- | | ||
| 21 | +| `theme` | string | the editor's own default (`turbo-classic`) | Name of the colour theme to start in, as listed by `turbo-golo -list-themes` | | ||
| 22 | +| `autosave` | boolean | `false` | Whether modified files are written without being asked. The file **Create project settings** writes sets it to `true`; the default here is what applies to a project with no settings file at all. | | ||
| 23 | +| `autosave_delay` | string | `"2s"` | How long after the last keystroke to wait. A Go duration: `"500ms"`, `"2s"`, `"1m"`. Only consulted when `autosave` is true. | | ||
| 24 | + | ||
| 25 | +### Example | ||
| 26 | + | ||
| 27 | +```toml | ||
| 28 | +[editor] | ||
| 29 | +theme = "turbo-dark" | ||
| 30 | +autosave = true | ||
| 31 | +autosave_delay = "500ms" | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +## Theme precedence | ||
| 35 | + | ||
| 36 | +Highest first: | ||
| 37 | + | ||
| 38 | +| Source | Wins over | | ||
| 39 | +| --- | --- | | ||
| 40 | +| `-theme` on the command line | everything | | ||
| 41 | +| `theme` in the settings file | the built-in default | | ||
| 42 | +| The built-in default `turbo-classic` | — | | ||
| 43 | + | ||
| 44 | +An unknown theme name at any level falls back to the built-in default rather than failing. | ||
| 45 | + | ||
| 46 | +## When a change takes effect | ||
| 47 | + | ||
| 48 | +The file is read at start-up, and **again every time it is saved from inside the editor** — so a change made in the editor is in force the moment you press `F2`, with no restart. | ||
| 49 | + | ||
| 50 | +| Key | Re-applied on save | Why | | ||
| 51 | +| --- | --- | --- | | ||
| 52 | +| `autosave` | yes | | | ||
| 53 | +| `autosave_delay` | yes | | | ||
| 54 | +| `theme` | **no** | Options ▸ Theme is the live way to change it, and already writes the choice back here. A `-theme` flag given on the command line is the more explicit statement for that session and is not overridden by a file being saved. | | ||
| 55 | + | ||
| 56 | +| Outcome | Status bar | | ||
| 57 | +| --- | --- | | ||
| 58 | +| Read and applied | `Applied .turbo-golo/settings.toml — autosave on (2s)` | | ||
| 59 | +| Read and applied, autosave off | `Applied .turbo-golo/settings.toml — autosave off` | | ||
| 60 | +| Saved, but no longer valid TOML | `Saved, but not applied: …` — the previous values stay in force | | ||
| 61 | + | ||
| 62 | +Saving is saving, whoever did it: automatic saving writing the settings file re-applies them exactly as `F2` does. Editing the file **outside** the editor is not noticed; nothing watches it. | ||
| 63 | + | ||
| 64 | +## Automatic saving | ||
| 65 | + | ||
| 66 | +| Behaviour | Detail | | ||
| 67 | +| --- | --- | | ||
| 68 | +| Trigger | The delay elapsing with no edit in any window | | ||
| 69 | +| Scope | Every open file with a name, not only the front one | | ||
| 70 | +| Deadline | One for the whole editor, restarted by any edit in any window | | ||
| 71 | +| Files with no name | Never saved; never asked about | | ||
| 72 | +| Report | `Saved <name>` on the status bar | | ||
| 73 | +| Failure | Reported on the status bar, never in a dialog, and not retried until the next edit | | ||
| 74 | +| Closing a window | Saves instead of asking, when the file has a name | | ||
| 75 | +| Leaving the editor | Saves instead of asking, when the file has a name | | ||
| 76 | + | ||
| 77 | +## Writes | ||
| 78 | + | ||
| 79 | +The settings file is written by exactly two actions. Nothing else in the editor writes to it, and nothing creates it by itself. | ||
| 80 | + | ||
| 81 | +| Action | Effect | | ||
| 82 | +| --- | --- | | ||
| 83 | +| **Options ▸ Create project settings** | Creates `.turbo-golo/settings.toml` with the theme in use, `autosave = true`, `autosave_delay = "2s"`, and explanatory comments. Greyed out once the project has one, so it cannot be chosen twice. | | ||
| 84 | +| **Options ▸ Theme** | Rewrites the `theme` value **only when the file already exists**. Comments, blank lines, key order and any trailing comment on the theme line are kept. | | ||
| 85 | + | ||
| 86 | +Both write through a temporary file in the same directory, renamed into place, so an interrupted write leaves the previous file intact. | ||
| 87 | + | ||
| 88 | +## Menu items | ||
| 89 | + | ||
| 90 | +| Item | Menu | Needs a file | Effect | | ||
| 91 | +| --- | --- | --- | --- | | ||
| 92 | +| Create project settings | Options | refuses the file | As above, then opens the file. Greyed out once the project has one. | | ||
| 93 | +| Project settings… | Options | requires the file | Opens `.turbo-golo/settings.toml`. Greyed out until the project has one. | | ||
| 94 | + | ||
| 95 | +## Errors | ||
| 96 | + | ||
| 97 | +| Message | Cause | | ||
| 98 | +| --- | --- | | ||
| 99 | +| `turbo-golo: reading …/settings.toml: …` on standard error | The file is present but is not valid TOML. The editor opens with its defaults. | | ||
| 100 | +| `reading …: autosave_delay "x" is not a duration such as "2s"` | `autosave_delay` is not a Go duration | | ||
| 101 | +| `reading …: autosave_delay must be positive, not "0s"` | `autosave_delay` is zero or negative | | ||
| 102 | +| `Already there: .turbo-golo/settings.toml` on the status bar | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. | | ||
| 103 | +| `Saved, but not applied: …` on the status bar | The settings file was written but no longer parses. The previous values stay in force. | | ||
| 104 | +| `This project has no .turbo-golo/settings.toml yet.` | **Project settings…** in a project that has none | | ||
| 105 | +| `Theme set for this session only: …` | The theme changed but the settings file could not be written | | ||
| 106 | + | ||
| 107 | +## See also | ||
| 108 | + | ||
| 109 | +- [How to give a project its own settings](../how-to/configure-a-project.md) | ||
| 110 | +- [Project settings](../explanation/project-settings.md) | ||
| 111 | +- [Theme file format](themes.md) — a different file, in the same language | ||
added
docs/en/reference/project-tree.md +102 -0 | new file mode 100644 | ||
| @@ -0,0 +1,102 @@ | ||
| 1 | +# Reference: project tree | |
| 2 | + | |
| 3 | +> Neutral description of the project tree window: what it shows, what it hides, and the keys it answers to. | |
| 4 | + | |
| 5 | +## Opening | |
| 6 | + | |
| 7 | +| Route | Condition | | |
| 8 | +| --- | --- | | |
| 9 | +| `F9` | Always | | |
| 10 | +| **Window ▸ Project tree** | Always | | |
| 11 | + | |
| 12 | +Neither requires a file to be open. Both bring the existing tree forward when one is already open: there is at most one tree window. | |
| 13 | + | |
| 14 | +## Root | |
| 15 | + | |
| 16 | +| Property | Value | | |
| 17 | +| --- | --- | | |
| 18 | +| Rooted at | The directory the editor was started in (`os.Getwd()`) | | |
| 19 | +| Search | That directory only. Parent directories are **not** searched, the same rule `.turbo-golo/settings.toml` follows. | | |
| 20 | +| Window title | The base name of that directory | | |
| 21 | +| Root row | Not shown; the first row is the first entry inside the project | | |
| 22 | + | |
| 23 | +## What is listed | |
| 24 | + | |
| 25 | +| Rule | Detail | | |
| 26 | +| --- | --- | | |
| 27 | +| Order | Directories first, then files; each group sorted by name | | |
| 28 | +| Hidden | `.git` only | | |
| 29 | +| Shown | Every other entry, dot-entries included — `.turbo-golo`, `.gitignore`, `.qlty` | | |
| 30 | +| Reading | A directory is read the first time it is expanded, and not before | | |
| 31 | +| Unreadable directory | Shows as expanded and empty; the rest of the tree is unaffected | | |
| 32 | + | |
| 33 | +## Markers | |
| 34 | + | |
| 35 | +| Marker | Meaning | | |
| 36 | +| --- | --- | | |
| 37 | +| `▶ ` | A directory that is closed | | |
| 38 | +| `▼ ` | A directory that is open | | |
| 39 | +| (two spaces) | A file — indented by a marker's width so names line up | | |
| 40 | + | |
| 41 | +Each level of depth adds two more spaces of indentation. | |
| 42 | + | |
| 43 | +## Keys | |
| 44 | + | |
| 45 | +Handled when the tree window has the focus. | |
| 46 | + | |
| 47 | +| Key | Action | | |
| 48 | +| --- | --- | | |
| 49 | +| `↑` `↓` | Previous / next row | | |
| 50 | +| `PgUp` `PgDn` | A screenful at a time | | |
| 51 | +| `Home` `End` | First / last row | | |
| 52 | +| `→` | Expand a closed directory; otherwise move to the next row | | |
| 53 | +| `←` | Collapse an open directory; otherwise move to the directory this row is in | | |
| 54 | +| `Enter` | Open a file; expand or collapse a directory | | |
| 55 | +| `F5`, `Ctrl-R` | Re-read the project | | |
| 56 | + | |
| 57 | +The editor's own shortcuts apply as usual: `F6` moves to the next window, `Ctrl-W` closes the tree, `Alt-X` leaves. | |
| 58 | + | |
| 59 | +## Mouse | |
| 60 | + | |
| 61 | +| Action | Effect | | |
| 62 | +| --- | --- | | |
| 63 | +| Click a row | Move the highlight to it | | |
| 64 | +| Click the highlighted row | Act on it, as `Enter` does | | |
| 65 | +| Wheel up / down | Move the highlight three rows | | |
| 66 | + | |
| 67 | +## Refreshing | |
| 68 | + | |
| 69 | +| Trigger | Effect | | |
| 70 | +| --- | --- | | |
| 71 | +| `F5` or `Ctrl-R` | Re-reads every directory that has been opened | | |
| 72 | +| Saving a file | The same, automatically | | |
| 73 | +| Expanding a directory | Reads that directory, if it has not been read | | |
| 74 | + | |
| 75 | +Refreshing keeps the shape of the tree: a directory that was open stays open, one that has been deleted takes its branch with it, and directories nobody has opened stay unread. The highlight stays on the same entry, or on the nearest remaining row when that entry has gone. | |
| 76 | + | |
| 77 | +The tree does **not** watch the filesystem. A file created by a terminal window — `golo new main`, `gogolo build` — or by `git checkout`, appears only after a refresh. | |
| 78 | + | |
| 79 | +## Colours | |
| 80 | + | |
| 81 | +| Theme key | What it colours | | |
| 82 | +| --- | --- | | |
| 83 | +| `tree.text` | A file's name, and the tree's background | | |
| 84 | +| `tree.directory` | A directory's name | | |
| 85 | +| `tree.selected` | The highlighted row, when the tree has the focus | | |
| 86 | +| `tree.unfocused` | The highlighted row, when it does not | | |
| 87 | + | |
| 88 | +These do not fall back to the `list.*` keys: dotted fallback runs along the dots and stops at `default`. See [Theme file format](themes.md). | |
| 89 | + | |
| 90 | +## Errors | |
| 91 | + | |
| 92 | +| Message | Cause | | |
| 93 | +| --- | --- | | |
| 94 | +| `Cannot tell which directory this is: …` | The working directory could not be read | | |
| 95 | +| `reading …: …` | The project directory could not be read | | |
| 96 | +| `… is not a directory` | The root resolved to a file | | |
| 97 | + | |
| 98 | +## See also | |
| 99 | + | |
| 100 | +- [How to browse a project and open files from a tree](../how-to/browse-a-project.md) | |
| 101 | +- [Project tree](../explanation/project-tree.md) | |
| 102 | +- [Keyboard](keyboard.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,102 @@ | |||
| 1 | +# Reference: project tree | ||
| 2 | + | ||
| 3 | +> Neutral description of the project tree window: what it shows, what it hides, and the keys it answers to. | ||
| 4 | + | ||
| 5 | +## Opening | ||
| 6 | + | ||
| 7 | +| Route | Condition | | ||
| 8 | +| --- | --- | | ||
| 9 | +| `F9` | Always | | ||
| 10 | +| **Window ▸ Project tree** | Always | | ||
| 11 | + | ||
| 12 | +Neither requires a file to be open. Both bring the existing tree forward when one is already open: there is at most one tree window. | ||
| 13 | + | ||
| 14 | +## Root | ||
| 15 | + | ||
| 16 | +| Property | Value | | ||
| 17 | +| --- | --- | | ||
| 18 | +| Rooted at | The directory the editor was started in (`os.Getwd()`) | | ||
| 19 | +| Search | That directory only. Parent directories are **not** searched, the same rule `.turbo-golo/settings.toml` follows. | | ||
| 20 | +| Window title | The base name of that directory | | ||
| 21 | +| Root row | Not shown; the first row is the first entry inside the project | | ||
| 22 | + | ||
| 23 | +## What is listed | ||
| 24 | + | ||
| 25 | +| Rule | Detail | | ||
| 26 | +| --- | --- | | ||
| 27 | +| Order | Directories first, then files; each group sorted by name | | ||
| 28 | +| Hidden | `.git` only | | ||
| 29 | +| Shown | Every other entry, dot-entries included — `.turbo-golo`, `.gitignore`, `.qlty` | | ||
| 30 | +| Reading | A directory is read the first time it is expanded, and not before | | ||
| 31 | +| Unreadable directory | Shows as expanded and empty; the rest of the tree is unaffected | | ||
| 32 | + | ||
| 33 | +## Markers | ||
| 34 | + | ||
| 35 | +| Marker | Meaning | | ||
| 36 | +| --- | --- | | ||
| 37 | +| `▶ ` | A directory that is closed | | ||
| 38 | +| `▼ ` | A directory that is open | | ||
| 39 | +| (two spaces) | A file — indented by a marker's width so names line up | | ||
| 40 | + | ||
| 41 | +Each level of depth adds two more spaces of indentation. | ||
| 42 | + | ||
| 43 | +## Keys | ||
| 44 | + | ||
| 45 | +Handled when the tree window has the focus. | ||
| 46 | + | ||
| 47 | +| Key | Action | | ||
| 48 | +| --- | --- | | ||
| 49 | +| `↑` `↓` | Previous / next row | | ||
| 50 | +| `PgUp` `PgDn` | A screenful at a time | | ||
| 51 | +| `Home` `End` | First / last row | | ||
| 52 | +| `→` | Expand a closed directory; otherwise move to the next row | | ||
| 53 | +| `←` | Collapse an open directory; otherwise move to the directory this row is in | | ||
| 54 | +| `Enter` | Open a file; expand or collapse a directory | | ||
| 55 | +| `F5`, `Ctrl-R` | Re-read the project | | ||
| 56 | + | ||
| 57 | +The editor's own shortcuts apply as usual: `F6` moves to the next window, `Ctrl-W` closes the tree, `Alt-X` leaves. | ||
| 58 | + | ||
| 59 | +## Mouse | ||
| 60 | + | ||
| 61 | +| Action | Effect | | ||
| 62 | +| --- | --- | | ||
| 63 | +| Click a row | Move the highlight to it | | ||
| 64 | +| Click the highlighted row | Act on it, as `Enter` does | | ||
| 65 | +| Wheel up / down | Move the highlight three rows | | ||
| 66 | + | ||
| 67 | +## Refreshing | ||
| 68 | + | ||
| 69 | +| Trigger | Effect | | ||
| 70 | +| --- | --- | | ||
| 71 | +| `F5` or `Ctrl-R` | Re-reads every directory that has been opened | | ||
| 72 | +| Saving a file | The same, automatically | | ||
| 73 | +| Expanding a directory | Reads that directory, if it has not been read | | ||
| 74 | + | ||
| 75 | +Refreshing keeps the shape of the tree: a directory that was open stays open, one that has been deleted takes its branch with it, and directories nobody has opened stay unread. The highlight stays on the same entry, or on the nearest remaining row when that entry has gone. | ||
| 76 | + | ||
| 77 | +The tree does **not** watch the filesystem. A file created by a terminal window — `golo new main`, `gogolo build` — or by `git checkout`, appears only after a refresh. | ||
| 78 | + | ||
| 79 | +## Colours | ||
| 80 | + | ||
| 81 | +| Theme key | What it colours | | ||
| 82 | +| --- | --- | | ||
| 83 | +| `tree.text` | A file's name, and the tree's background | | ||
| 84 | +| `tree.directory` | A directory's name | | ||
| 85 | +| `tree.selected` | The highlighted row, when the tree has the focus | | ||
| 86 | +| `tree.unfocused` | The highlighted row, when it does not | | ||
| 87 | + | ||
| 88 | +These do not fall back to the `list.*` keys: dotted fallback runs along the dots and stops at `default`. See [Theme file format](themes.md). | ||
| 89 | + | ||
| 90 | +## Errors | ||
| 91 | + | ||
| 92 | +| Message | Cause | | ||
| 93 | +| --- | --- | | ||
| 94 | +| `Cannot tell which directory this is: …` | The working directory could not be read | | ||
| 95 | +| `reading …: …` | The project directory could not be read | | ||
| 96 | +| `… is not a directory` | The root resolved to a file | | ||
| 97 | + | ||
| 98 | +## See also | ||
| 99 | + | ||
| 100 | +- [How to browse a project and open files from a tree](../how-to/browse-a-project.md) | ||
| 101 | +- [Project tree](../explanation/project-tree.md) | ||
| 102 | +- [Keyboard](keyboard.md) | ||
added
docs/en/reference/snippets.md +132 -0 | new file mode 100644 | ||
| @@ -0,0 +1,132 @@ | ||
| 1 | +# Reference: snippets | |
| 2 | + | |
| 3 | +> Neutral description of the snippets files, the Snippets menu, and how a snippet is inserted. | |
| 4 | + | |
| 5 | +## Files | |
| 6 | + | |
| 7 | +Both are read, and both are optional. | |
| 8 | + | |
| 9 | +| File | Holds | | |
| 10 | +| --- | --- | | |
| 11 | +| `./.turbo-golo/snippets.toml` | The project's snippets | | |
| 12 | +| `$TURBO_GOLO_SNIPPET_DIR/snippets.toml`, else `<user config>/turbo-golo/snippets.toml` | Your own, shared across projects | | |
| 13 | + | |
| 14 | +`<user config>` is `os.UserConfigDir()`: `~/.config` on Linux, `~/Library/Application Support` on macOS. `TURBO_GOLO_DIR` replaces it when set. | |
| 15 | + | |
| 16 | +| Property | Value | | |
| 17 | +| --- | --- | | |
| 18 | +| Project search | The working directory only. Parent directories are **not** searched. | | |
| 19 | +| Read | Every time the Snippets menu opens | | |
| 20 | +| Order | Your own first, then the project's | | |
| 21 | +| Name clash | Same `group` **and** `name` → the project's replaces yours | | |
| 22 | +| Missing file | Not an error | | |
| 23 | +| Unreadable file | An error, shown in the menu | | |
| 24 | + | |
| 25 | +## File format | |
| 26 | + | |
| 27 | +One `[[snippet]]` table per snippet. | |
| 28 | + | |
| 29 | +| Key | Type | Required | Description | | |
| 30 | +| --- | --- | --- | --- | | |
| 31 | +| `name` | string | yes | What the menu shows | | |
| 32 | +| `body` | string | yes | The text inserted at the cursor | | |
| 33 | +| `group` | string | no | The submenu it goes in; absent means `General` | | |
| 34 | +| `languages` | array of strings | no | Restricts the snippet to those languages; absent means every file | | |
| 35 | + | |
| 36 | +`languages` uses the editor's own language names: `golo`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash`. See [Languages coloured](languages.md). | |
| 37 | + | |
| 38 | +A snippet with no `name` or no `body` makes the whole file an error — it could not be shown or could not be inserted. | |
| 39 | + | |
| 40 | +### Example | |
| 41 | + | |
| 42 | +```toml | |
| 43 | +[[snippet]] | |
| 44 | +name = "try" | |
| 45 | +group = "Golo" | |
| 46 | +languages = ["golo"] | |
| 47 | +body = ''' | |
| 48 | +try { | |
| 49 | + throw "boom" | |
| 50 | +} catch (e) { | |
| 51 | + println("caught: \"" + e + "\"") | |
| 52 | +} finally { | |
| 53 | + println("done") | |
| 54 | +}''' | |
| 55 | +``` | |
| 56 | + | |
| 57 | +TOML's `'''` literal strings drop the newline immediately after the opening quotes and keep every backslash as it is written — which is what a Golo body needs, since Golo strings carry `\n` and `\"`. A `"""` basic string also drops that first newline, but resolves `\t`, `\n` and `\"` before the editor sees them. | |
| 58 | + | |
| 59 | +## The starter file | |
| 60 | + | |
| 61 | +**Snippets ▸ Create snippets file** writes fourteen snippets: | |
| 62 | + | |
| 63 | +| Group | Names | `languages` | | |
| 64 | +| --- | --- | --- | | |
| 65 | +| Golo | `module`, `main`, `function`, `closure`, `struct`, `union`, `augment`, `match`, `foreach`, `for`, `try`, `comprehension` | `["golo"]` | | |
| 66 | +| General | `Hello` | none | | |
| 67 | +| Markdown | `Image` | `["markdown"]` | | |
| 68 | + | |
| 69 | +The Golo bodies are indented with two spaces and written as literal strings. | |
| 70 | + | |
| 71 | +## The menu | |
| 72 | + | |
| 73 | +| Item | Condition | | |
| 74 | +| --- | --- | | |
| 75 | +| One submenu per group, in the order the groups first appear in the files | A group with at least one snippet applying to the front window | | |
| 76 | +| `Cannot read snippets`, greyed out | A file is present but unreadable | | |
| 77 | +| `Create snippets file` | The project has no snippets file | | |
| 78 | +| `Open snippets file` | The project has one | | |
| 79 | + | |
| 80 | +The menu's hot key is `Alt-N`, not `Alt-S`: Search already answers to S. | |
| 81 | + | |
| 82 | +Groups, and the snippets inside them, come out in the order they were read, so the menu matches the files. | |
| 83 | + | |
| 84 | +A snippet item is greyed out when there is no file open to insert into — a terminal or the project tree in front counts as no file. | |
| 85 | + | |
| 86 | +### Filtering | |
| 87 | + | |
| 88 | +| Front window | Snippets offered | | |
| 89 | +| --- | --- | | |
| 90 | +| A file of a recognised language | Those naming that language, plus those naming none | | |
| 91 | +| A file of no recognised language | Those naming none | | |
| 92 | +| A terminal, the project tree, or nothing | Those naming none | | |
| 93 | + | |
| 94 | +A `.golo` file is of language `golo`; so is a file with no extension whose first line is a shebang naming `golo`. | |
| 95 | + | |
| 96 | +## Insertion | |
| 97 | + | |
| 98 | +| Behaviour | Detail | | |
| 99 | +| --- | --- | | |
| 100 | +| Position | At the cursor | | |
| 101 | +| First line | Inserted where the cursor is | | |
| 102 | +| Later lines | Prefixed with the leading whitespace of the line the cursor was on | | |
| 103 | +| Blank lines in the body | Left blank, not padded with whitespace | | |
| 104 | +| Undo | One step for the whole snippet | | |
| 105 | +| Cursor after | At the end of the inserted text | | |
| 106 | +| Report | `Snippet inserted` on the status bar | | |
| 107 | + | |
| 108 | +The indent copied is the **whitespace prefix of the current line**, tabs or spaces as they were, so a snippet follows whatever the file already uses. | |
| 109 | + | |
| 110 | +## Menu items | |
| 111 | + | |
| 112 | +| Item | Menu | Effect | | |
| 113 | +| --- | --- | --- | | |
| 114 | +| Create snippets file | Snippets | Writes `.turbo-golo/snippets.toml` with the starter snippets above, then opens it. Greyed out once the project has one. | | |
| 115 | +| Open snippets file | Snippets | Opens `.turbo-golo/snippets.toml`. Greyed out until the project has one. Always the project's file, never your own — it is the file the item above it writes. | | |
| 116 | + | |
| 117 | +The file is written through a temporary file in the same directory, renamed into place, so an interrupted write leaves the previous file intact. | |
| 118 | + | |
| 119 | +## Errors | |
| 120 | + | |
| 121 | +| Message | Cause | | |
| 122 | +| --- | --- | | |
| 123 | +| `Cannot read snippets` in the menu | A snippets file is present but not valid TOML, or holds a snippet with no name or no body | | |
| 124 | +| `Already there: .turbo-golo/snippets.toml` | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. | | |
| 125 | +| `This project has no .turbo-golo/snippets.toml yet.` | Opening in a project that has none, likewise | | |
| 126 | +| `Cannot tell which directory this is: …` | The working directory could not be read | | |
| 127 | + | |
| 128 | +## See also | |
| 129 | + | |
| 130 | +- [How to insert snippets from a menu](../how-to/use-snippets.md) | |
| 131 | +- [Snippets](../explanation/snippets.md) | |
| 132 | +- [Keyboard](keyboard.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,132 @@ | |||
| 1 | +# Reference: snippets | ||
| 2 | + | ||
| 3 | +> Neutral description of the snippets files, the Snippets menu, and how a snippet is inserted. | ||
| 4 | + | ||
| 5 | +## Files | ||
| 6 | + | ||
| 7 | +Both are read, and both are optional. | ||
| 8 | + | ||
| 9 | +| File | Holds | | ||
| 10 | +| --- | --- | | ||
| 11 | +| `./.turbo-golo/snippets.toml` | The project's snippets | | ||
| 12 | +| `$TURBO_GOLO_SNIPPET_DIR/snippets.toml`, else `<user config>/turbo-golo/snippets.toml` | Your own, shared across projects | | ||
| 13 | + | ||
| 14 | +`<user config>` is `os.UserConfigDir()`: `~/.config` on Linux, `~/Library/Application Support` on macOS. `TURBO_GOLO_DIR` replaces it when set. | ||
| 15 | + | ||
| 16 | +| Property | Value | | ||
| 17 | +| --- | --- | | ||
| 18 | +| Project search | The working directory only. Parent directories are **not** searched. | | ||
| 19 | +| Read | Every time the Snippets menu opens | | ||
| 20 | +| Order | Your own first, then the project's | | ||
| 21 | +| Name clash | Same `group` **and** `name` → the project's replaces yours | | ||
| 22 | +| Missing file | Not an error | | ||
| 23 | +| Unreadable file | An error, shown in the menu | | ||
| 24 | + | ||
| 25 | +## File format | ||
| 26 | + | ||
| 27 | +One `[[snippet]]` table per snippet. | ||
| 28 | + | ||
| 29 | +| Key | Type | Required | Description | | ||
| 30 | +| --- | --- | --- | --- | | ||
| 31 | +| `name` | string | yes | What the menu shows | | ||
| 32 | +| `body` | string | yes | The text inserted at the cursor | | ||
| 33 | +| `group` | string | no | The submenu it goes in; absent means `General` | | ||
| 34 | +| `languages` | array of strings | no | Restricts the snippet to those languages; absent means every file | | ||
| 35 | + | ||
| 36 | +`languages` uses the editor's own language names: `golo`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash`. See [Languages coloured](languages.md). | ||
| 37 | + | ||
| 38 | +A snippet with no `name` or no `body` makes the whole file an error — it could not be shown or could not be inserted. | ||
| 39 | + | ||
| 40 | +### Example | ||
| 41 | + | ||
| 42 | +```toml | ||
| 43 | +[[snippet]] | ||
| 44 | +name = "try" | ||
| 45 | +group = "Golo" | ||
| 46 | +languages = ["golo"] | ||
| 47 | +body = ''' | ||
| 48 | +try { | ||
| 49 | + throw "boom" | ||
| 50 | +} catch (e) { | ||
| 51 | + println("caught: \"" + e + "\"") | ||
| 52 | +} finally { | ||
| 53 | + println("done") | ||
| 54 | +}''' | ||
| 55 | +``` | ||
| 56 | + | ||
| 57 | +TOML's `'''` literal strings drop the newline immediately after the opening quotes and keep every backslash as it is written — which is what a Golo body needs, since Golo strings carry `\n` and `\"`. A `"""` basic string also drops that first newline, but resolves `\t`, `\n` and `\"` before the editor sees them. | ||
| 58 | + | ||
| 59 | +## The starter file | ||
| 60 | + | ||
| 61 | +**Snippets ▸ Create snippets file** writes fourteen snippets: | ||
| 62 | + | ||
| 63 | +| Group | Names | `languages` | | ||
| 64 | +| --- | --- | --- | | ||
| 65 | +| Golo | `module`, `main`, `function`, `closure`, `struct`, `union`, `augment`, `match`, `foreach`, `for`, `try`, `comprehension` | `["golo"]` | | ||
| 66 | +| General | `Hello` | none | | ||
| 67 | +| Markdown | `Image` | `["markdown"]` | | ||
| 68 | + | ||
| 69 | +The Golo bodies are indented with two spaces and written as literal strings. | ||
| 70 | + | ||
| 71 | +## The menu | ||
| 72 | + | ||
| 73 | +| Item | Condition | | ||
| 74 | +| --- | --- | | ||
| 75 | +| One submenu per group, in the order the groups first appear in the files | A group with at least one snippet applying to the front window | | ||
| 76 | +| `Cannot read snippets`, greyed out | A file is present but unreadable | | ||
| 77 | +| `Create snippets file` | The project has no snippets file | | ||
| 78 | +| `Open snippets file` | The project has one | | ||
| 79 | + | ||
| 80 | +The menu's hot key is `Alt-N`, not `Alt-S`: Search already answers to S. | ||
| 81 | + | ||
| 82 | +Groups, and the snippets inside them, come out in the order they were read, so the menu matches the files. | ||
| 83 | + | ||
| 84 | +A snippet item is greyed out when there is no file open to insert into — a terminal or the project tree in front counts as no file. | ||
| 85 | + | ||
| 86 | +### Filtering | ||
| 87 | + | ||
| 88 | +| Front window | Snippets offered | | ||
| 89 | +| --- | --- | | ||
| 90 | +| A file of a recognised language | Those naming that language, plus those naming none | | ||
| 91 | +| A file of no recognised language | Those naming none | | ||
| 92 | +| A terminal, the project tree, or nothing | Those naming none | | ||
| 93 | + | ||
| 94 | +A `.golo` file is of language `golo`; so is a file with no extension whose first line is a shebang naming `golo`. | ||
| 95 | + | ||
| 96 | +## Insertion | ||
| 97 | + | ||
| 98 | +| Behaviour | Detail | | ||
| 99 | +| --- | --- | | ||
| 100 | +| Position | At the cursor | | ||
| 101 | +| First line | Inserted where the cursor is | | ||
| 102 | +| Later lines | Prefixed with the leading whitespace of the line the cursor was on | | ||
| 103 | +| Blank lines in the body | Left blank, not padded with whitespace | | ||
| 104 | +| Undo | One step for the whole snippet | | ||
| 105 | +| Cursor after | At the end of the inserted text | | ||
| 106 | +| Report | `Snippet inserted` on the status bar | | ||
| 107 | + | ||
| 108 | +The indent copied is the **whitespace prefix of the current line**, tabs or spaces as they were, so a snippet follows whatever the file already uses. | ||
| 109 | + | ||
| 110 | +## Menu items | ||
| 111 | + | ||
| 112 | +| Item | Menu | Effect | | ||
| 113 | +| --- | --- | --- | | ||
| 114 | +| Create snippets file | Snippets | Writes `.turbo-golo/snippets.toml` with the starter snippets above, then opens it. Greyed out once the project has one. | | ||
| 115 | +| Open snippets file | Snippets | Opens `.turbo-golo/snippets.toml`. Greyed out until the project has one. Always the project's file, never your own — it is the file the item above it writes. | | ||
| 116 | + | ||
| 117 | +The file is written through a temporary file in the same directory, renamed into place, so an interrupted write leaves the previous file intact. | ||
| 118 | + | ||
| 119 | +## Errors | ||
| 120 | + | ||
| 121 | +| Message | Cause | | ||
| 122 | +| --- | --- | | ||
| 123 | +| `Cannot read snippets` in the menu | A snippets file is present but not valid TOML, or holds a snippet with no name or no body | | ||
| 124 | +| `Already there: .turbo-golo/snippets.toml` | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. | | ||
| 125 | +| `This project has no .turbo-golo/snippets.toml yet.` | Opening in a project that has none, likewise | | ||
| 126 | +| `Cannot tell which directory this is: …` | The working directory could not be read | | ||
| 127 | + | ||
| 128 | +## See also | ||
| 129 | + | ||
| 130 | +- [How to insert snippets from a menu](../how-to/use-snippets.md) | ||
| 131 | +- [Snippets](../explanation/snippets.md) | ||
| 132 | +- [Keyboard](keyboard.md) | ||
added
docs/en/reference/terminal.md +228 -0 | new file mode 100644 | ||
| @@ -0,0 +1,228 @@ | ||
| 1 | +# Reference: terminal windows | |
| 2 | + | |
| 3 | +> Neutral description of the terminal windows Turbo Golo opens, the keys they answer to, and the escape sequences the emulator implements. | |
| 4 | + | |
| 5 | +## Opening | |
| 6 | + | |
| 7 | +| Route | Condition | | |
| 8 | +| --- | --- | | |
| 9 | +| `F8` | Always | | |
| 10 | +| **Window ▸ New terminal** | Always | | |
| 11 | + | |
| 12 | +Neither requires a file to be open. A tool in `.turbo-golo/tools.toml` whose `output` is `terminal` also opens one, running that command instead of a shell; see [Golo tools](golo-tools.md). | |
| 13 | + | |
| 14 | +## The shell | |
| 15 | + | |
| 16 | +| Property | Value | | |
| 17 | +| --- | --- | | |
| 18 | +| Program | `$SHELL`, or `/bin/sh` when it is unset or empty; on Windows `%COMSPEC%`, or `cmd.exe` | | |
| 19 | +| Working directory | The directory of the file in the front window; the editor's working directory when no file is open | | |
| 20 | +| `TERM` | `xterm-256color`, always — replacing any inherited value | | |
| 21 | +| Environment | The editor's own, with `TERM` replaced | | |
| 22 | +| Controlling terminal | Yes: on Linux and macOS the shell runs in its own session with the pseudo-terminal as its controlling terminal; on Windows it is attached to a pseudo-console. Either way job control and `Ctrl-C` work | | |
| 23 | +| Initial size | The window's, updated whenever the window is resized | | |
| 24 | + | |
| 25 | +## Platform support | |
| 26 | + | |
| 27 | +| Platform | Behaviour | | |
| 28 | +| --- | --- | | |
| 29 | +| Linux | Supported (`/dev/ptmx`) | | |
| 30 | +| macOS | Supported (`/dev/ptmx`) | | |
| 31 | +| Windows | Supported (pseudo-console, ConPTY): Windows 10 version 1809 or later. Built and vetted; **not yet run by the authors** on a Windows machine | | |
| 32 | +| Others | `F8` opens a message saying terminal windows are not supported yet; nothing else changes | | |
| 33 | + | |
| 34 | +## Keys | |
| 35 | + | |
| 36 | +### After the program has gone | |
| 37 | + | |
| 38 | +A window whose command has finished keeps its output, but stops behaving like a terminal: only `Shift-PgUp` and `Shift-PgDn` are still taken, and every other key reaches the editor — which is what lets `Ctrl-W` close it. | |
| 39 | + | |
| 40 | +### Sent to the shell | |
| 41 | + | |
| 42 | +Every key not listed under "kept by the editor" below, encoded as a terminal expects it. | |
| 43 | + | |
| 44 | +| Key | Bytes sent | | |
| 45 | +| --- | --- | | |
| 46 | +| printable character | its UTF-8 encoding | | |
| 47 | +| `Alt-<key>` | `ESC` followed by that key's own bytes | | |
| 48 | +| `Ctrl-A` … `Ctrl-Z` | `0x01` … `0x1a` | | |
| 49 | +| `Enter` | `\r` | | |
| 50 | +| `Tab` | `\t` | | |
| 51 | +| `Shift-Tab` | `ESC [ Z` | | |
| 52 | +| `Backspace` | `0x7f` | | |
| 53 | +| `Escape` | `0x1b` | | |
| 54 | +| `↑` `↓` `→` `←` | `ESC [ A B C D`, or `ESC O A B C D` in application cursor mode | | |
| 55 | +| `Home` `End` | `ESC [ H`, `ESC [ F`, or the `ESC O` forms in application cursor mode | | |
| 56 | +| `Insert` `Delete` | `ESC [ 2~`, `ESC [ 3~` | | |
| 57 | +| `PgUp` `PgDn` | `ESC [ 5~`, `ESC [ 6~` | | |
| 58 | +| `F1` … `F4` | `ESC O P Q R S` | | |
| 59 | +| `F5` … `F12` | `ESC [ 15~ 17~ 18~ 19~ 20~ 21~ 23~ 24~` | | |
| 60 | + | |
| 61 | +A key with no terminal meaning sends nothing. | |
| 62 | + | |
| 63 | +### Kept by the editor | |
| 64 | + | |
| 65 | +| Key | Action | | |
| 66 | +| --- | --- | | |
| 67 | +| `F1` … `F12` | Their usual editor action | | |
| 68 | +| `Alt-X` | Exit | | |
| 69 | +| `Alt-0` … `Alt-9` | List windows / bring window 1…9 forward | | |
| 70 | + | |
| 71 | +Function keys therefore never reach a program inside a terminal window. | |
| 72 | + | |
| 73 | +### Handled by the terminal window itself | |
| 74 | + | |
| 75 | +| Key | Action | | |
| 76 | +| --- | --- | | |
| 77 | +| `Shift-PgUp` | Back one screenful through the history | | |
| 78 | +| `Shift-PgDn` | Forward one screenful | | |
| 79 | + | |
| 80 | +Any key sent to the shell also returns the view to the live screen. | |
| 81 | + | |
| 82 | +## Mouse | |
| 83 | + | |
| 84 | +| Action | Effect | | |
| 85 | +| --- | --- | | |
| 86 | +| Wheel up / down | Scroll three lines through the history | | |
| 87 | +| Click | Brings the window forward; not forwarded to the program | | |
| 88 | + | |
| 89 | +Mouse reporting is not implemented, so a program is never told about clicks. | |
| 90 | + | |
| 91 | +## History | |
| 92 | + | |
| 93 | +| Property | Value | | |
| 94 | +| --- | --- | | |
| 95 | +| Lines kept | 2000 | | |
| 96 | +| What is kept | Lines scrolled off the top of the primary screen only | | |
| 97 | +| Alternate screen | Not kept — a full-screen program leaves no history behind | | |
| 98 | + | |
| 99 | +## Emulation | |
| 100 | + | |
| 101 | +`TERM` is `xterm-256color`. What is implemented of it: | |
| 102 | + | |
| 103 | +### Control characters | |
| 104 | + | |
| 105 | +| Byte | Effect | | |
| 106 | +| --- | --- | | |
| 107 | +| `0x07` BEL | Noted; the editor does not sound it | | |
| 108 | +| `0x08` BS | Cursor left one column | | |
| 109 | +| `0x09` HT | To the next tab stop, every 8 columns | | |
| 110 | +| `0x0a` `0x0b` `0x0c` | Line feed | | |
| 111 | +| `0x0d` CR | To column 1 | | |
| 112 | + | |
| 113 | +### Escape sequences | |
| 114 | + | |
| 115 | +| Sequence | Name | Effect | | |
| 116 | +| --- | --- | --- | | |
| 117 | +| `ESC D` | IND | Line feed | | |
| 118 | +| `ESC E` | NEL | Carriage return and line feed | | |
| 119 | +| `ESC M` | RI | Reverse line feed, keeping the column | | |
| 120 | +| `ESC 7` | DECSC | Save cursor and style | | |
| 121 | +| `ESC 8` | DECRC | Restore cursor and style | | |
| 122 | +| `ESC c` | RIS | Full reset | | |
| 123 | + | |
| 124 | +### CSI sequences | |
| 125 | + | |
| 126 | +| Sequence | Name | Effect | | |
| 127 | +| --- | --- | --- | | |
| 128 | +| `CSI n A B C D` | CUU CUD CUF CUB | Move n cells up, down, right, left | | |
| 129 | +| `CSI n E F` | CNL CPL | n lines down / up, to column 1 | | |
| 130 | +| `CSI n G` | CHA | To column n | | |
| 131 | +| `CSI r ; c H`, `CSI r ; c f` | CUP HVP | To row r, column c | | |
| 132 | +| `CSI n d` | VPA | To row n | | |
| 133 | +| `CSI n J` | ED | Erase display: 0 to end, 1 to start, 2 or 3 all | | |
| 134 | +| `CSI n K` | EL | Erase line: 0 to end, 1 to start, 2 all | | |
| 135 | +| `CSI n L` | IL | Insert n blank lines at the cursor | | |
| 136 | +| `CSI n M` | DL | Delete n lines at the cursor | | |
| 137 | +| `CSI n @` | ICH | Insert n blank cells | | |
| 138 | +| `CSI n P` | DCH | Delete n cells | | |
| 139 | +| `CSI n X` | ECH | Erase n cells in place | | |
| 140 | +| `CSI n S` | SU | Scroll the region up n lines | | |
| 141 | +| `CSI n T` | SD | Scroll the region down n lines | | |
| 142 | +| `CSI t ; b r` | DECSTBM | Set the scroll region to rows t…b | | |
| 143 | +| `CSI s`, `CSI u` | SCP RCP | Save / restore the cursor | | |
| 144 | +| `CSI … m` | SGR | Colours and attributes, below | | |
| 145 | + | |
| 146 | +`IL` and `DL` do nothing when the cursor is outside the scroll region. | |
| 147 | + | |
| 148 | +### Private modes | |
| 149 | + | |
| 150 | +Set with `CSI ? n h`, cleared with `CSI ? n l`. | |
| 151 | + | |
| 152 | +| n | Name | Effect | | |
| 153 | +| --- | --- | --- | | |
| 154 | +| 1 | DECCKM | Application cursor keys: arrows send `ESC O x` | | |
| 155 | +| 7 | DECAWM | Auto-wrap at the right margin | | |
| 156 | +| 25 | DECTCEM | Show the cursor | | |
| 157 | +| 47, 1047 | | Alternate screen | | |
| 158 | +| 1048 | | Save / restore the cursor | | |
| 159 | +| 1049 | | Save the cursor, then the alternate screen | | |
| 160 | + | |
| 161 | +Any other mode is parsed and ignored. | |
| 162 | + | |
| 163 | +### SGR | |
| 164 | + | |
| 165 | +| Code | Effect | | |
| 166 | +| --- | --- | | |
| 167 | +| 0 | Reset | | |
| 168 | +| 1, 22 | Bold on / off | | |
| 169 | +| 2, 22 | Dim on / off | | |
| 170 | +| 3, 23 | Italic on / off | | |
| 171 | +| 4, 24 | Underline on / off | | |
| 172 | +| 5, 6, 25 | Blink on / off | | |
| 173 | +| 7, 27 | Reverse on / off | | |
| 174 | +| 9, 29 | Strike-through on / off | | |
| 175 | +| 30–37, 40–47 | The eight normal colours, foreground / background | | |
| 176 | +| 90–97, 100–107 | The eight bright colours, foreground / background | | |
| 177 | +| 38;5;n, 48;5;n | Palette colour n of 256 | | |
| 178 | +| 38;2;r;g;b, 48;2;r;g;b | 24-bit colour | | |
| 179 | +| 39, 49 | Back to the theme's colour | | |
| 180 | + | |
| 181 | +The sixteen named colours are tcell's, which means the palette the user's own terminal is configured with, not fixed hex values. An extended colour that runs out of parameters partway leaves the style unchanged. Any other code is ignored. | |
| 182 | + | |
| 183 | +### OSC | |
| 184 | + | |
| 185 | +| Sequence | Effect | | |
| 186 | +| --- | --- | | |
| 187 | +| `OSC 0 ; text BEL`, `OSC 2 ; text BEL` | Set the window title | | |
| 188 | +| `OSC … ST` | The `ESC \` terminator is accepted in place of BEL | | |
| 189 | + | |
| 190 | +The title is capped at 4096 bytes. Other OSC commands are parsed and ignored. | |
| 191 | + | |
| 192 | +### Consumed and ignored | |
| 193 | + | |
| 194 | +Parsed correctly, so they never appear as stray characters, but with no effect: | |
| 195 | + | |
| 196 | +| Sequence | Name | | |
| 197 | +| --- | --- | | |
| 198 | +| `ESC P …`, `ESC X …`, `ESC ^ …`, `ESC _ …` | DCS, SOS, PM, APC — read to their string terminator | | |
| 199 | +| `ESC (`, `ESC )`, `ESC *`, `ESC +`, `ESC %`, `ESC #`, `ESC <space>` | Character-set and line-size selectors — the emulator works in UTF-8 regardless | | |
| 200 | +| `CSI ? n h`, `CSI ? n l` for any other n | Private modes not listed above | | |
| 201 | +| Any other CSI final byte, SGR code, or OSC command | | | |
| 202 | + | |
| 203 | +### Not implemented | |
| 204 | + | |
| 205 | +Mouse reporting, bracketed paste, shift-in / shift-out, double-width lines, sixel and other graphics protocols, and the DEC status and device-attribute reports. A program that asks for one of these gets no reply, so a program that waits for one waits forever. | |
| 206 | + | |
| 207 | +## Colours | |
| 208 | + | |
| 209 | +| Theme key | What it colours | | |
| 210 | +| --- | --- | | |
| 211 | +| `terminal.text` | Every cell whose colour the program did not choose | | |
| 212 | +| `terminal.cursor` | The cell under the cursor, when the window has the focus | | |
| 213 | + | |
| 214 | +See [Theme file format](themes.md). | |
| 215 | + | |
| 216 | +## Errors | |
| 217 | + | |
| 218 | +| Message | Cause | | |
| 219 | +| --- | --- | | |
| 220 | +| Terminal windows are not supported on this platform yet | The build has no pseudo-terminal support: any platform other than Linux, macOS and Windows | | |
| 221 | +| `openpt: …`, `grantpt: …`, `ptsname: …` | The operating system refused to open a pseudo-terminal | | |
| 222 | +| `fork/exec …: no such file or directory` | `$SHELL` names a program that does not exist | | |
| 223 | + | |
| 224 | +## See also | |
| 225 | + | |
| 226 | +- [How to run shell commands without leaving the editor](../how-to/use-a-terminal.md) | |
| 227 | +- [Terminal windows](../explanation/terminal-windows.md) | |
| 228 | +- [Keyboard](keyboard.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,228 @@ | |||
| 1 | +# Reference: terminal windows | ||
| 2 | + | ||
| 3 | +> Neutral description of the terminal windows Turbo Golo opens, the keys they answer to, and the escape sequences the emulator implements. | ||
| 4 | + | ||
| 5 | +## Opening | ||
| 6 | + | ||
| 7 | +| Route | Condition | | ||
| 8 | +| --- | --- | | ||
| 9 | +| `F8` | Always | | ||
| 10 | +| **Window ▸ New terminal** | Always | | ||
| 11 | + | ||
| 12 | +Neither requires a file to be open. A tool in `.turbo-golo/tools.toml` whose `output` is `terminal` also opens one, running that command instead of a shell; see [Golo tools](golo-tools.md). | ||
| 13 | + | ||
| 14 | +## The shell | ||
| 15 | + | ||
| 16 | +| Property | Value | | ||
| 17 | +| --- | --- | | ||
| 18 | +| Program | `$SHELL`, or `/bin/sh` when it is unset or empty; on Windows `%COMSPEC%`, or `cmd.exe` | | ||
| 19 | +| Working directory | The directory of the file in the front window; the editor's working directory when no file is open | | ||
| 20 | +| `TERM` | `xterm-256color`, always — replacing any inherited value | | ||
| 21 | +| Environment | The editor's own, with `TERM` replaced | | ||
| 22 | +| Controlling terminal | Yes: on Linux and macOS the shell runs in its own session with the pseudo-terminal as its controlling terminal; on Windows it is attached to a pseudo-console. Either way job control and `Ctrl-C` work | | ||
| 23 | +| Initial size | The window's, updated whenever the window is resized | | ||
| 24 | + | ||
| 25 | +## Platform support | ||
| 26 | + | ||
| 27 | +| Platform | Behaviour | | ||
| 28 | +| --- | --- | | ||
| 29 | +| Linux | Supported (`/dev/ptmx`) | | ||
| 30 | +| macOS | Supported (`/dev/ptmx`) | | ||
| 31 | +| Windows | Supported (pseudo-console, ConPTY): Windows 10 version 1809 or later. Built and vetted; **not yet run by the authors** on a Windows machine | | ||
| 32 | +| Others | `F8` opens a message saying terminal windows are not supported yet; nothing else changes | | ||
| 33 | + | ||
| 34 | +## Keys | ||
| 35 | + | ||
| 36 | +### After the program has gone | ||
| 37 | + | ||
| 38 | +A window whose command has finished keeps its output, but stops behaving like a terminal: only `Shift-PgUp` and `Shift-PgDn` are still taken, and every other key reaches the editor — which is what lets `Ctrl-W` close it. | ||
| 39 | + | ||
| 40 | +### Sent to the shell | ||
| 41 | + | ||
| 42 | +Every key not listed under "kept by the editor" below, encoded as a terminal expects it. | ||
| 43 | + | ||
| 44 | +| Key | Bytes sent | | ||
| 45 | +| --- | --- | | ||
| 46 | +| printable character | its UTF-8 encoding | | ||
| 47 | +| `Alt-<key>` | `ESC` followed by that key's own bytes | | ||
| 48 | +| `Ctrl-A` … `Ctrl-Z` | `0x01` … `0x1a` | | ||
| 49 | +| `Enter` | `\r` | | ||
| 50 | +| `Tab` | `\t` | | ||
| 51 | +| `Shift-Tab` | `ESC [ Z` | | ||
| 52 | +| `Backspace` | `0x7f` | | ||
| 53 | +| `Escape` | `0x1b` | | ||
| 54 | +| `↑` `↓` `→` `←` | `ESC [ A B C D`, or `ESC O A B C D` in application cursor mode | | ||
| 55 | +| `Home` `End` | `ESC [ H`, `ESC [ F`, or the `ESC O` forms in application cursor mode | | ||
| 56 | +| `Insert` `Delete` | `ESC [ 2~`, `ESC [ 3~` | | ||
| 57 | +| `PgUp` `PgDn` | `ESC [ 5~`, `ESC [ 6~` | | ||
| 58 | +| `F1` … `F4` | `ESC O P Q R S` | | ||
| 59 | +| `F5` … `F12` | `ESC [ 15~ 17~ 18~ 19~ 20~ 21~ 23~ 24~` | | ||
| 60 | + | ||
| 61 | +A key with no terminal meaning sends nothing. | ||
| 62 | + | ||
| 63 | +### Kept by the editor | ||
| 64 | + | ||
| 65 | +| Key | Action | | ||
| 66 | +| --- | --- | | ||
| 67 | +| `F1` … `F12` | Their usual editor action | | ||
| 68 | +| `Alt-X` | Exit | | ||
| 69 | +| `Alt-0` … `Alt-9` | List windows / bring window 1…9 forward | | ||
| 70 | + | ||
| 71 | +Function keys therefore never reach a program inside a terminal window. | ||
| 72 | + | ||
| 73 | +### Handled by the terminal window itself | ||
| 74 | + | ||
| 75 | +| Key | Action | | ||
| 76 | +| --- | --- | | ||
| 77 | +| `Shift-PgUp` | Back one screenful through the history | | ||
| 78 | +| `Shift-PgDn` | Forward one screenful | | ||
| 79 | + | ||
| 80 | +Any key sent to the shell also returns the view to the live screen. | ||
| 81 | + | ||
| 82 | +## Mouse | ||
| 83 | + | ||
| 84 | +| Action | Effect | | ||
| 85 | +| --- | --- | | ||
| 86 | +| Wheel up / down | Scroll three lines through the history | | ||
| 87 | +| Click | Brings the window forward; not forwarded to the program | | ||
| 88 | + | ||
| 89 | +Mouse reporting is not implemented, so a program is never told about clicks. | ||
| 90 | + | ||
| 91 | +## History | ||
| 92 | + | ||
| 93 | +| Property | Value | | ||
| 94 | +| --- | --- | | ||
| 95 | +| Lines kept | 2000 | | ||
| 96 | +| What is kept | Lines scrolled off the top of the primary screen only | | ||
| 97 | +| Alternate screen | Not kept — a full-screen program leaves no history behind | | ||
| 98 | + | ||
| 99 | +## Emulation | ||
| 100 | + | ||
| 101 | +`TERM` is `xterm-256color`. What is implemented of it: | ||
| 102 | + | ||
| 103 | +### Control characters | ||
| 104 | + | ||
| 105 | +| Byte | Effect | | ||
| 106 | +| --- | --- | | ||
| 107 | +| `0x07` BEL | Noted; the editor does not sound it | | ||
| 108 | +| `0x08` BS | Cursor left one column | | ||
| 109 | +| `0x09` HT | To the next tab stop, every 8 columns | | ||
| 110 | +| `0x0a` `0x0b` `0x0c` | Line feed | | ||
| 111 | +| `0x0d` CR | To column 1 | | ||
| 112 | + | ||
| 113 | +### Escape sequences | ||
| 114 | + | ||
| 115 | +| Sequence | Name | Effect | | ||
| 116 | +| --- | --- | --- | | ||
| 117 | +| `ESC D` | IND | Line feed | | ||
| 118 | +| `ESC E` | NEL | Carriage return and line feed | | ||
| 119 | +| `ESC M` | RI | Reverse line feed, keeping the column | | ||
| 120 | +| `ESC 7` | DECSC | Save cursor and style | | ||
| 121 | +| `ESC 8` | DECRC | Restore cursor and style | | ||
| 122 | +| `ESC c` | RIS | Full reset | | ||
| 123 | + | ||
| 124 | +### CSI sequences | ||
| 125 | + | ||
| 126 | +| Sequence | Name | Effect | | ||
| 127 | +| --- | --- | --- | | ||
| 128 | +| `CSI n A B C D` | CUU CUD CUF CUB | Move n cells up, down, right, left | | ||
| 129 | +| `CSI n E F` | CNL CPL | n lines down / up, to column 1 | | ||
| 130 | +| `CSI n G` | CHA | To column n | | ||
| 131 | +| `CSI r ; c H`, `CSI r ; c f` | CUP HVP | To row r, column c | | ||
| 132 | +| `CSI n d` | VPA | To row n | | ||
| 133 | +| `CSI n J` | ED | Erase display: 0 to end, 1 to start, 2 or 3 all | | ||
| 134 | +| `CSI n K` | EL | Erase line: 0 to end, 1 to start, 2 all | | ||
| 135 | +| `CSI n L` | IL | Insert n blank lines at the cursor | | ||
| 136 | +| `CSI n M` | DL | Delete n lines at the cursor | | ||
| 137 | +| `CSI n @` | ICH | Insert n blank cells | | ||
| 138 | +| `CSI n P` | DCH | Delete n cells | | ||
| 139 | +| `CSI n X` | ECH | Erase n cells in place | | ||
| 140 | +| `CSI n S` | SU | Scroll the region up n lines | | ||
| 141 | +| `CSI n T` | SD | Scroll the region down n lines | | ||
| 142 | +| `CSI t ; b r` | DECSTBM | Set the scroll region to rows t…b | | ||
| 143 | +| `CSI s`, `CSI u` | SCP RCP | Save / restore the cursor | | ||
| 144 | +| `CSI … m` | SGR | Colours and attributes, below | | ||
| 145 | + | ||
| 146 | +`IL` and `DL` do nothing when the cursor is outside the scroll region. | ||
| 147 | + | ||
| 148 | +### Private modes | ||
| 149 | + | ||
| 150 | +Set with `CSI ? n h`, cleared with `CSI ? n l`. | ||
| 151 | + | ||
| 152 | +| n | Name | Effect | | ||
| 153 | +| --- | --- | --- | | ||
| 154 | +| 1 | DECCKM | Application cursor keys: arrows send `ESC O x` | | ||
| 155 | +| 7 | DECAWM | Auto-wrap at the right margin | | ||
| 156 | +| 25 | DECTCEM | Show the cursor | | ||
| 157 | +| 47, 1047 | | Alternate screen | | ||
| 158 | +| 1048 | | Save / restore the cursor | | ||
| 159 | +| 1049 | | Save the cursor, then the alternate screen | | ||
| 160 | + | ||
| 161 | +Any other mode is parsed and ignored. | ||
| 162 | + | ||
| 163 | +### SGR | ||
| 164 | + | ||
| 165 | +| Code | Effect | | ||
| 166 | +| --- | --- | | ||
| 167 | +| 0 | Reset | | ||
| 168 | +| 1, 22 | Bold on / off | | ||
| 169 | +| 2, 22 | Dim on / off | | ||
| 170 | +| 3, 23 | Italic on / off | | ||
| 171 | +| 4, 24 | Underline on / off | | ||
| 172 | +| 5, 6, 25 | Blink on / off | | ||
| 173 | +| 7, 27 | Reverse on / off | | ||
| 174 | +| 9, 29 | Strike-through on / off | | ||
| 175 | +| 30–37, 40–47 | The eight normal colours, foreground / background | | ||
| 176 | +| 90–97, 100–107 | The eight bright colours, foreground / background | | ||
| 177 | +| 38;5;n, 48;5;n | Palette colour n of 256 | | ||
| 178 | +| 38;2;r;g;b, 48;2;r;g;b | 24-bit colour | | ||
| 179 | +| 39, 49 | Back to the theme's colour | | ||
| 180 | + | ||
| 181 | +The sixteen named colours are tcell's, which means the palette the user's own terminal is configured with, not fixed hex values. An extended colour that runs out of parameters partway leaves the style unchanged. Any other code is ignored. | ||
| 182 | + | ||
| 183 | +### OSC | ||
| 184 | + | ||
| 185 | +| Sequence | Effect | | ||
| 186 | +| --- | --- | | ||
| 187 | +| `OSC 0 ; text BEL`, `OSC 2 ; text BEL` | Set the window title | | ||
| 188 | +| `OSC … ST` | The `ESC \` terminator is accepted in place of BEL | | ||
| 189 | + | ||
| 190 | +The title is capped at 4096 bytes. Other OSC commands are parsed and ignored. | ||
| 191 | + | ||
| 192 | +### Consumed and ignored | ||
| 193 | + | ||
| 194 | +Parsed correctly, so they never appear as stray characters, but with no effect: | ||
| 195 | + | ||
| 196 | +| Sequence | Name | | ||
| 197 | +| --- | --- | | ||
| 198 | +| `ESC P …`, `ESC X …`, `ESC ^ …`, `ESC _ …` | DCS, SOS, PM, APC — read to their string terminator | | ||
| 199 | +| `ESC (`, `ESC )`, `ESC *`, `ESC +`, `ESC %`, `ESC #`, `ESC <space>` | Character-set and line-size selectors — the emulator works in UTF-8 regardless | | ||
| 200 | +| `CSI ? n h`, `CSI ? n l` for any other n | Private modes not listed above | | ||
| 201 | +| Any other CSI final byte, SGR code, or OSC command | | | ||
| 202 | + | ||
| 203 | +### Not implemented | ||
| 204 | + | ||
| 205 | +Mouse reporting, bracketed paste, shift-in / shift-out, double-width lines, sixel and other graphics protocols, and the DEC status and device-attribute reports. A program that asks for one of these gets no reply, so a program that waits for one waits forever. | ||
| 206 | + | ||
| 207 | +## Colours | ||
| 208 | + | ||
| 209 | +| Theme key | What it colours | | ||
| 210 | +| --- | --- | | ||
| 211 | +| `terminal.text` | Every cell whose colour the program did not choose | | ||
| 212 | +| `terminal.cursor` | The cell under the cursor, when the window has the focus | | ||
| 213 | + | ||
| 214 | +See [Theme file format](themes.md). | ||
| 215 | + | ||
| 216 | +## Errors | ||
| 217 | + | ||
| 218 | +| Message | Cause | | ||
| 219 | +| --- | --- | | ||
| 220 | +| Terminal windows are not supported on this platform yet | The build has no pseudo-terminal support: any platform other than Linux, macOS and Windows | | ||
| 221 | +| `openpt: …`, `grantpt: …`, `ptsname: …` | The operating system refused to open a pseudo-terminal | | ||
| 222 | +| `fork/exec …: no such file or directory` | `$SHELL` names a program that does not exist | | ||
| 223 | + | ||
| 224 | +## See also | ||
| 225 | + | ||
| 226 | +- [How to run shell commands without leaving the editor](../how-to/use-a-terminal.md) | ||
| 227 | +- [Terminal windows](../explanation/terminal-windows.md) | ||
| 228 | +- [Keyboard](keyboard.md) | ||
added
docs/en/reference/themes.md +238 -0 | new file mode 100644 | ||
| @@ -0,0 +1,238 @@ | ||
| 1 | +# Reference: theme file format | |
| 2 | + | |
| 3 | +> Neutral, exhaustive description of a Turbo Golo theme file. | |
| 4 | + | |
| 5 | +A theme is a TOML file. Themes are read from the user theme directory first, then from the ones embedded in the binary; a user file wins over an embedded theme of the same name. | |
| 6 | + | |
| 7 | +## Locations | |
| 8 | + | |
| 9 | +| Location | Notes | | |
| 10 | +| --- | --- | | |
| 11 | +| `$TURBO_GOLO_THEME_DIR` | Used when the variable is set and non-empty. | | |
| 12 | +| `~/.config/turbo-golo/themes` | Linux (`os.UserConfigDir`); `TURBO_GOLO_DIR` replaces the `~/.config/turbo-golo` part when set. | | |
| 13 | +| `~/Library/Application Support/turbo-golo/themes` | macOS. | | |
| 14 | +| embedded | `turbo-classic`, `turbo-dark`, `borland-light`, `cappuccino`, `catppuccin-frappe`, `catppuccin-latte`, `cobalt`, `darcula`, `intellij-light`, `monochrome-dark`, `monochrome-light`. | | |
| 15 | + | |
| 16 | +A theme's **name** for `-theme` and for `Options ▸ Theme…` is its file name without `.toml`. It may not contain `/`, `\` or `..`. | |
| 17 | + | |
| 18 | +## The themes that ship | |
| 19 | + | |
| 20 | +| Name | Ground | For | | |
| 21 | +| --- | --- | --- | | |
| 22 | +| `turbo-classic` | Borland navy | The default: the palette Turbo C had | | |
| 23 | +| `turbo-dark` | Neutral dark grey | Modern terminals with true colour | | |
| 24 | +| `borland-light` | Paper white | Bright rooms and projectors | | |
| 25 | +| `cappuccino` | Espresso brown | The Turbo layout with the temperature up: milk in the text, caramel where Turbo Dark puts blue | | |
| 26 | +| `catppuccin-frappe` | Warm slate | The Catppuccin Frappé palette, unchanged: pastel accents on a soft dark ground | | |
| 27 | +| `catppuccin-latte` | Warm paper | The Catppuccin Latte palette, unchanged: the same mapping with the saturation a light ground needs | | |
| 28 | +| `cobalt` | Deep navy | The Cobalt palette, accents kept as loud as they are known for | | |
| 29 | +| `darcula` | Charcoal | After JetBrains' Darcula: orange keywords, green strings, and the orange punctuation that makes it recognisable | | |
| 30 | +| `intellij-light` | White | After JetBrains' IntelliJ Light: blue bold keywords, green bold strings | | |
| 31 | +| `monochrome-dark` | Black and greys | No hue at all — code told apart by lightness, bold, italic and underline | | |
| 32 | +| `monochrome-light` | Paper and greys | The same, the other way up: on paper the darkest grey is the loudest | | |
| 33 | + | |
| 34 | +Every one of them **states its whole palette** rather than inheriting most of it. A theme you write yourself may inherit; see [writing one](../how-to/write-a-theme.md). | |
| 35 | + | |
| 36 | +## A name a theme used to answer to | |
| 37 | + | |
| 38 | +`monochrome` still loads. It is what this theme shipped as before `monochrome-light` joined it and the pair was renamed, and a settings file or a `-theme` flag saying `monochrome` gets `monochrome-dark`. | |
| 39 | + | |
| 40 | +| Retired name | Loads | | |
| 41 | +| --- | --- | | |
| 42 | +| `monochrome` | `monochrome-dark` | | |
| 43 | + | |
| 44 | +A retired name is **not** listed by `-list-themes` or by **Options ▸ Theme…**, so each theme appears once, under the name it has now. A theme of your own called `monochrome.toml` still wins over it, exactly as it would for any other name. | |
| 45 | + | |
| 46 | +## Top-level fields | |
| 47 | + | |
| 48 | +| Field | Type | Default | Description | | |
| 49 | +| --- | --- | --- | --- | | |
| 50 | +| `name` | string | the file's base name | Display name, shown in the theme picker and the About box. | | |
| 51 | +| `description` | string | `""` | One line, shown by `-list-themes`. | | |
| 52 | +| `inherits` | string | none | Name of a theme to start from. Its resolved styles are the base; this file overrides what it names. Chains are capped at 16 hops. | | |
| 53 | +| `colors` | table | `{}` | The styles. Keys are the style keys below. | | |
| 54 | + | |
| 55 | +## Entry fields | |
| 56 | + | |
| 57 | +Each value under `[colors]` is an inline table: | |
| 58 | + | |
| 59 | +| Field | Type | Default | Description | | |
| 60 | +| --- | --- | --- | --- | | |
| 61 | +| `fg` | string | inherited | Foreground colour. | | |
| 62 | +| `bg` | string | inherited | Background colour. | | |
| 63 | +| `bold` | bool | `false` | Switch bold on. | | |
| 64 | +| `underline` | bool | `false` | Switch underline on. | | |
| 65 | +| `italic` | bool | `false` | Switch italic on. | | |
| 66 | +| `reverse` | bool | `false` | Swap foreground and background. | | |
| 67 | +| `dim` | bool | `false` | Switch dim on. | | |
| 68 | +| `blink` | bool | `false` | Switch blink on. | | |
| 69 | + | |
| 70 | +Attributes are only ever switched **on**; there is no way to switch an inherited attribute off other than by not inheriting it. | |
| 71 | + | |
| 72 | +## Colour values | |
| 73 | + | |
| 74 | +| Form | Example | Notes | | |
| 75 | +| --- | --- | --- | | |
| 76 | +| ANSI name | `navy`, `aqua`, `silver`, `fuchsia` | The sixteen names, plus the full W3C list. | | |
| 77 | +| Hex literal | `#5fafd7` | 24-bit; tcell approximates it on terminals without true colour. | | |
| 78 | +| `default` | `default` | Whatever the terminal itself uses. | | |
| 79 | +| `-` | `-` | Same as `default`. | | |
| 80 | +| `""` | `""` | Same as `default`. | | |
| 81 | + | |
| 82 | +The sixteen ANSI names: `black` `maroon` `green` `olive` `navy` `purple` `teal` `silver` `gray` `red` `lime` `yellow` `blue` `fuchsia` `aqua` `white`. | |
| 83 | + | |
| 84 | +An unrecognised colour is a **load error**, not a silent fallback. | |
| 85 | + | |
| 86 | +## Style keys | |
| 87 | + | |
| 88 | +Undefined keys fall back along the dots, and finally to `default`. | |
| 89 | + | |
| 90 | +### Base | |
| 91 | + | |
| 92 | +| Key | What it colours | | |
| 93 | +| --- | --- | | |
| 94 | +| `default` | The last resort of every lookup | | |
| 95 | +| `desktop` | The patterned backdrop behind the windows | | |
| 96 | +| `shadow` | The cells a window darkens behind itself | | |
| 97 | + | |
| 98 | +### Menu bar | |
| 99 | + | |
| 100 | +| Key | What it colours | | |
| 101 | +| --- | --- | | |
| 102 | +| `menu.bar` | The row of titles | | |
| 103 | +| `menu.item` | A drop-down entry | | |
| 104 | +| `menu.selected` | The highlighted entry | | |
| 105 | +| `menu.shortcut` | The hot letter of a label | | |
| 106 | +| `menu.disabled` | An entry that cannot be chosen | | |
| 107 | + | |
| 108 | +### Windows | |
| 109 | + | |
| 110 | +| Key | What it colours | | |
| 111 | +| --- | --- | | |
| 112 | +| `window.frame.active` | The frame of the focused window | | |
| 113 | +| `window.frame.inactive` | Every other frame | | |
| 114 | +| `window.title.active` | The focused window's title | | |
| 115 | +| `window.title.inactive` | Every other title | | |
| 116 | +| `window.body` | The interior, before its content draws | | |
| 117 | + | |
| 118 | +### Bars | |
| 119 | + | |
| 120 | +| Key | What it colours | | |
| 121 | +| --- | --- | | |
| 122 | +| `statusbar` | The bar itself | | |
| 123 | +| `statusbar.key` | The `Fn` part of a hint | | |
| 124 | +| `statusbar.hint` | The right-aligned text | | |
| 125 | +| `scrollbar` | A scroll bar's track | | |
| 126 | +| `scrollbar.thumb` | Its thumb and arrows | | |
| 127 | + | |
| 128 | +### Dialogs and controls | |
| 129 | + | |
| 130 | +| Key | What it colours | | |
| 131 | +| --- | --- | | |
| 132 | +| `dialog.frame` | A dialog's frame | | |
| 133 | +| `dialog.body` | Its interior | | |
| 134 | +| `dialog.title` | Its title | | |
| 135 | +| `dialog.label` | A line of static text | | |
| 136 | +| `button` | A button | | |
| 137 | +| `button.focused` | The focused button | | |
| 138 | +| `button.shortcut` | The hot letter of a button | | |
| 139 | +| `input` | An input field | | |
| 140 | +| `input.focused` | The focused input field | | |
| 141 | +| `input.selection` | Selected text in an input field | | |
| 142 | +| `list` | A list box | | |
| 143 | +| `list.selected` | Its highlighted line, when focused | | |
| 144 | +| `list.unfocused` | Its highlighted line, when not | | |
| 145 | +| `checkbox` | A check box | | |
| 146 | +| `checkbox.focused` | The focused check box | | |
| 147 | + | |
| 148 | +### Editor | |
| 149 | + | |
| 150 | +| Key | What it colours | | |
| 151 | +| --- | --- | | |
| 152 | +| `editor.text` | Text no other rule claims | | |
| 153 | +| `editor.selection` | Selected text | | |
| 154 | +| `editor.linenumber` | The line-number gutter | | |
| 155 | +| `editor.currentline` | The line the cursor is on | | |
| 156 | +| `editor.cursor` | The cursor. Its **background** is also sent to the terminal as its cursor colour, and its foreground paints the character underneath. | | |
| 157 | + | |
| 158 | +### Terminal | |
| 159 | + | |
| 160 | +| Key | What it colours | | |
| 161 | +| --- | --- | | |
| 162 | +| `terminal.text` | Every cell of a terminal window whose colour the program running in it did not choose | | |
| 163 | +| `terminal.cursor` | The cell under a terminal's cursor, when that window has the focus | | |
| 164 | + | |
| 165 | +A program that names its own colours keeps them: these two only fill in what it left unset. See [Terminal windows](terminal.md). | |
| 166 | + | |
| 167 | +### Project tree | |
| 168 | + | |
| 169 | +| Key | What it colours | | |
| 170 | +| --- | --- | | |
| 171 | +| `tree.text` | A file's name in the project tree, and the tree's background | | |
| 172 | +| `tree.directory` | A directory's name | | |
| 173 | +| `tree.selected` | The highlighted row, when the tree has the focus | | |
| 174 | +| `tree.unfocused` | The highlighted row, when it does not | | |
| 175 | + | |
| 176 | +These are separate from the `list.*` keys on purpose: a dialog's list is coloured against a dialog, and reusing it would highlight a tree row in the very colour a window's body already is. See [Project tree](project-tree.md). | |
| 177 | + | |
| 178 | +### Syntax | |
| 179 | + | |
| 180 | +The examples are Golo's; the other eight languages map their own constructs onto the same classes. | |
| 181 | + | |
| 182 | +| Key | What it colours | | |
| 183 | +| --- | --- | | |
| 184 | +| `syntax.identifier` | An ordinary name — `args`, `item`, `this` | | |
| 185 | +| `syntax.keyword` | `function`, `let`, `module`, `import`, `foreach`, `match`, `when`, … | | |
| 186 | +| `syntax.type` | A capitalised name — `Point`, `Shape`, `Circle` — and the dotted path after `module` or `import` | | |
| 187 | +| `syntax.builtin` | `println`, `str`, `len`, `list`, `map`, `range`, … | | |
| 188 | +| `syntax.constant` | `true`, `false`, `null` | | |
| 189 | +| `syntax.function` | A lower-case name before `(`, or the name after `function` | | |
| 190 | +| `syntax.string` | `"…"` and `"""…"""` | | |
| 191 | +| `syntax.char` | A `'c'` character literal | | |
| 192 | +| `syntax.number` | `42`, `3.14`, `1e3`, `42L`, `2.0F` | | |
| 193 | +| `syntax.comment` | `#` to the end of the line, and `----` … `----` | | |
| 194 | +| `syntax.operator` | `+`, `->`, `==`, `?:`, `..`, … | | |
| 195 | +| `syntax.punctuation` | Brackets, commas, dots, semicolons, and the `$` of `Shape$Circle` | | |
| 196 | +| `syntax.heading` | A Markdown heading, whole line | | |
| 197 | +| `syntax.tag` | An HTML element name and its brackets | | |
| 198 | +| `syntax.attribute` | An HTML attribute's name | | |
| 199 | +| `syntax.emphasis` | Markdown bold and italic | | |
| 200 | +| `syntax.link` | A Markdown link or image | | |
| 201 | + | |
| 202 | +Which language produces which class is in [Languages coloured](languages.md). | |
| 203 | + | |
| 204 | +### Completion and diagnostics | |
| 205 | + | |
| 206 | +| Key | What it colours | | |
| 207 | +| --- | --- | | |
| 208 | +| `completion.frame` | The popup's frame | | |
| 209 | +| `completion.item` | A suggestion | | |
| 210 | +| `completion.selected` | The highlighted suggestion | | |
| 211 | +| `completion.detail` | The kind tag beside a suggestion | | |
| 212 | +| `diagnostic.error` | An error from the language server | | |
| 213 | +| `diagnostic.warning` | A warning | | |
| 214 | +| `diagnostic.info` | A note | | |
| 215 | + | |
| 216 | +## Example | |
| 217 | + | |
| 218 | +```toml | |
| 219 | +name = "Mine" | |
| 220 | +description = "Turbo Classic, with readable comments." | |
| 221 | +inherits = "turbo-classic" | |
| 222 | + | |
| 223 | +[colors] | |
| 224 | +"syntax.comment" = { fg = "#8a8a8a", italic = true } | |
| 225 | +"syntax.string" = { fg = "#87d7af" } | |
| 226 | +"editor.currentline" = { bg = "#00005f" } | |
| 227 | +``` | |
| 228 | + | |
| 229 | +## Errors | |
| 230 | + | |
| 231 | +| Message | Cause | | |
| 232 | +| --- | --- | | |
| 233 | +| `theme: not found: "x"` | No `x.toml` in the user directory or among the embedded themes. | | |
| 234 | +| `theme: not found: "…" is not a plain theme name` | The name contains `/`, `\` or `..`. | | |
| 235 | +| `invalid TOML: …` | The file is not valid TOML. | | |
| 236 | +| `colors."k": fg: unknown colour "…"` | The colour name is not recognised. | | |
| 237 | +| `inherits: chain deeper than 16, probably a loop` | Two themes inherit from each other, directly or through others. | | |
| 238 | +| `inherits "x": theme: not found` | The parent named does not exist. | | |
| new file mode 100644 | |||
| @@ -0,0 +1,238 @@ | |||
| 1 | +# Reference: theme file format | ||
| 2 | + | ||
| 3 | +> Neutral, exhaustive description of a Turbo Golo theme file. | ||
| 4 | + | ||
| 5 | +A theme is a TOML file. Themes are read from the user theme directory first, then from the ones embedded in the binary; a user file wins over an embedded theme of the same name. | ||
| 6 | + | ||
| 7 | +## Locations | ||
| 8 | + | ||
| 9 | +| Location | Notes | | ||
| 10 | +| --- | --- | | ||
| 11 | +| `$TURBO_GOLO_THEME_DIR` | Used when the variable is set and non-empty. | | ||
| 12 | +| `~/.config/turbo-golo/themes` | Linux (`os.UserConfigDir`); `TURBO_GOLO_DIR` replaces the `~/.config/turbo-golo` part when set. | | ||
| 13 | +| `~/Library/Application Support/turbo-golo/themes` | macOS. | | ||
| 14 | +| embedded | `turbo-classic`, `turbo-dark`, `borland-light`, `cappuccino`, `catppuccin-frappe`, `catppuccin-latte`, `cobalt`, `darcula`, `intellij-light`, `monochrome-dark`, `monochrome-light`. | | ||
| 15 | + | ||
| 16 | +A theme's **name** for `-theme` and for `Options ▸ Theme…` is its file name without `.toml`. It may not contain `/`, `\` or `..`. | ||
| 17 | + | ||
| 18 | +## The themes that ship | ||
| 19 | + | ||
| 20 | +| Name | Ground | For | | ||
| 21 | +| --- | --- | --- | | ||
| 22 | +| `turbo-classic` | Borland navy | The default: the palette Turbo C had | | ||
| 23 | +| `turbo-dark` | Neutral dark grey | Modern terminals with true colour | | ||
| 24 | +| `borland-light` | Paper white | Bright rooms and projectors | | ||
| 25 | +| `cappuccino` | Espresso brown | The Turbo layout with the temperature up: milk in the text, caramel where Turbo Dark puts blue | | ||
| 26 | +| `catppuccin-frappe` | Warm slate | The Catppuccin Frappé palette, unchanged: pastel accents on a soft dark ground | | ||
| 27 | +| `catppuccin-latte` | Warm paper | The Catppuccin Latte palette, unchanged: the same mapping with the saturation a light ground needs | | ||
| 28 | +| `cobalt` | Deep navy | The Cobalt palette, accents kept as loud as they are known for | | ||
| 29 | +| `darcula` | Charcoal | After JetBrains' Darcula: orange keywords, green strings, and the orange punctuation that makes it recognisable | | ||
| 30 | +| `intellij-light` | White | After JetBrains' IntelliJ Light: blue bold keywords, green bold strings | | ||
| 31 | +| `monochrome-dark` | Black and greys | No hue at all — code told apart by lightness, bold, italic and underline | | ||
| 32 | +| `monochrome-light` | Paper and greys | The same, the other way up: on paper the darkest grey is the loudest | | ||
| 33 | + | ||
| 34 | +Every one of them **states its whole palette** rather than inheriting most of it. A theme you write yourself may inherit; see [writing one](../how-to/write-a-theme.md). | ||
| 35 | + | ||
| 36 | +## A name a theme used to answer to | ||
| 37 | + | ||
| 38 | +`monochrome` still loads. It is what this theme shipped as before `monochrome-light` joined it and the pair was renamed, and a settings file or a `-theme` flag saying `monochrome` gets `monochrome-dark`. | ||
| 39 | + | ||
| 40 | +| Retired name | Loads | | ||
| 41 | +| --- | --- | | ||
| 42 | +| `monochrome` | `monochrome-dark` | | ||
| 43 | + | ||
| 44 | +A retired name is **not** listed by `-list-themes` or by **Options ▸ Theme…**, so each theme appears once, under the name it has now. A theme of your own called `monochrome.toml` still wins over it, exactly as it would for any other name. | ||
| 45 | + | ||
| 46 | +## Top-level fields | ||
| 47 | + | ||
| 48 | +| Field | Type | Default | Description | | ||
| 49 | +| --- | --- | --- | --- | | ||
| 50 | +| `name` | string | the file's base name | Display name, shown in the theme picker and the About box. | | ||
| 51 | +| `description` | string | `""` | One line, shown by `-list-themes`. | | ||
| 52 | +| `inherits` | string | none | Name of a theme to start from. Its resolved styles are the base; this file overrides what it names. Chains are capped at 16 hops. | | ||
| 53 | +| `colors` | table | `{}` | The styles. Keys are the style keys below. | | ||
| 54 | + | ||
| 55 | +## Entry fields | ||
| 56 | + | ||
| 57 | +Each value under `[colors]` is an inline table: | ||
| 58 | + | ||
| 59 | +| Field | Type | Default | Description | | ||
| 60 | +| --- | --- | --- | --- | | ||
| 61 | +| `fg` | string | inherited | Foreground colour. | | ||
| 62 | +| `bg` | string | inherited | Background colour. | | ||
| 63 | +| `bold` | bool | `false` | Switch bold on. | | ||
| 64 | +| `underline` | bool | `false` | Switch underline on. | | ||
| 65 | +| `italic` | bool | `false` | Switch italic on. | | ||
| 66 | +| `reverse` | bool | `false` | Swap foreground and background. | | ||
| 67 | +| `dim` | bool | `false` | Switch dim on. | | ||
| 68 | +| `blink` | bool | `false` | Switch blink on. | | ||
| 69 | + | ||
| 70 | +Attributes are only ever switched **on**; there is no way to switch an inherited attribute off other than by not inheriting it. | ||
| 71 | + | ||
| 72 | +## Colour values | ||
| 73 | + | ||
| 74 | +| Form | Example | Notes | | ||
| 75 | +| --- | --- | --- | | ||
| 76 | +| ANSI name | `navy`, `aqua`, `silver`, `fuchsia` | The sixteen names, plus the full W3C list. | | ||
| 77 | +| Hex literal | `#5fafd7` | 24-bit; tcell approximates it on terminals without true colour. | | ||
| 78 | +| `default` | `default` | Whatever the terminal itself uses. | | ||
| 79 | +| `-` | `-` | Same as `default`. | | ||
| 80 | +| `""` | `""` | Same as `default`. | | ||
| 81 | + | ||
| 82 | +The sixteen ANSI names: `black` `maroon` `green` `olive` `navy` `purple` `teal` `silver` `gray` `red` `lime` `yellow` `blue` `fuchsia` `aqua` `white`. | ||
| 83 | + | ||
| 84 | +An unrecognised colour is a **load error**, not a silent fallback. | ||
| 85 | + | ||
| 86 | +## Style keys | ||
| 87 | + | ||
| 88 | +Undefined keys fall back along the dots, and finally to `default`. | ||
| 89 | + | ||
| 90 | +### Base | ||
| 91 | + | ||
| 92 | +| Key | What it colours | | ||
| 93 | +| --- | --- | | ||
| 94 | +| `default` | The last resort of every lookup | | ||
| 95 | +| `desktop` | The patterned backdrop behind the windows | | ||
| 96 | +| `shadow` | The cells a window darkens behind itself | | ||
| 97 | + | ||
| 98 | +### Menu bar | ||
| 99 | + | ||
| 100 | +| Key | What it colours | | ||
| 101 | +| --- | --- | | ||
| 102 | +| `menu.bar` | The row of titles | | ||
| 103 | +| `menu.item` | A drop-down entry | | ||
| 104 | +| `menu.selected` | The highlighted entry | | ||
| 105 | +| `menu.shortcut` | The hot letter of a label | | ||
| 106 | +| `menu.disabled` | An entry that cannot be chosen | | ||
| 107 | + | ||
| 108 | +### Windows | ||
| 109 | + | ||
| 110 | +| Key | What it colours | | ||
| 111 | +| --- | --- | | ||
| 112 | +| `window.frame.active` | The frame of the focused window | | ||
| 113 | +| `window.frame.inactive` | Every other frame | | ||
| 114 | +| `window.title.active` | The focused window's title | | ||
| 115 | +| `window.title.inactive` | Every other title | | ||
| 116 | +| `window.body` | The interior, before its content draws | | ||
| 117 | + | ||
| 118 | +### Bars | ||
| 119 | + | ||
| 120 | +| Key | What it colours | | ||
| 121 | +| --- | --- | | ||
| 122 | +| `statusbar` | The bar itself | | ||
| 123 | +| `statusbar.key` | The `Fn` part of a hint | | ||
| 124 | +| `statusbar.hint` | The right-aligned text | | ||
| 125 | +| `scrollbar` | A scroll bar's track | | ||
| 126 | +| `scrollbar.thumb` | Its thumb and arrows | | ||
| 127 | + | ||
| 128 | +### Dialogs and controls | ||
| 129 | + | ||
| 130 | +| Key | What it colours | | ||
| 131 | +| --- | --- | | ||
| 132 | +| `dialog.frame` | A dialog's frame | | ||
| 133 | +| `dialog.body` | Its interior | | ||
| 134 | +| `dialog.title` | Its title | | ||
| 135 | +| `dialog.label` | A line of static text | | ||
| 136 | +| `button` | A button | | ||
| 137 | +| `button.focused` | The focused button | | ||
| 138 | +| `button.shortcut` | The hot letter of a button | | ||
| 139 | +| `input` | An input field | | ||
| 140 | +| `input.focused` | The focused input field | | ||
| 141 | +| `input.selection` | Selected text in an input field | | ||
| 142 | +| `list` | A list box | | ||
| 143 | +| `list.selected` | Its highlighted line, when focused | | ||
| 144 | +| `list.unfocused` | Its highlighted line, when not | | ||
| 145 | +| `checkbox` | A check box | | ||
| 146 | +| `checkbox.focused` | The focused check box | | ||
| 147 | + | ||
| 148 | +### Editor | ||
| 149 | + | ||
| 150 | +| Key | What it colours | | ||
| 151 | +| --- | --- | | ||
| 152 | +| `editor.text` | Text no other rule claims | | ||
| 153 | +| `editor.selection` | Selected text | | ||
| 154 | +| `editor.linenumber` | The line-number gutter | | ||
| 155 | +| `editor.currentline` | The line the cursor is on | | ||
| 156 | +| `editor.cursor` | The cursor. Its **background** is also sent to the terminal as its cursor colour, and its foreground paints the character underneath. | | ||
| 157 | + | ||
| 158 | +### Terminal | ||
| 159 | + | ||
| 160 | +| Key | What it colours | | ||
| 161 | +| --- | --- | | ||
| 162 | +| `terminal.text` | Every cell of a terminal window whose colour the program running in it did not choose | | ||
| 163 | +| `terminal.cursor` | The cell under a terminal's cursor, when that window has the focus | | ||
| 164 | + | ||
| 165 | +A program that names its own colours keeps them: these two only fill in what it left unset. See [Terminal windows](terminal.md). | ||
| 166 | + | ||
| 167 | +### Project tree | ||
| 168 | + | ||
| 169 | +| Key | What it colours | | ||
| 170 | +| --- | --- | | ||
| 171 | +| `tree.text` | A file's name in the project tree, and the tree's background | | ||
| 172 | +| `tree.directory` | A directory's name | | ||
| 173 | +| `tree.selected` | The highlighted row, when the tree has the focus | | ||
| 174 | +| `tree.unfocused` | The highlighted row, when it does not | | ||
| 175 | + | ||
| 176 | +These are separate from the `list.*` keys on purpose: a dialog's list is coloured against a dialog, and reusing it would highlight a tree row in the very colour a window's body already is. See [Project tree](project-tree.md). | ||
| 177 | + | ||
| 178 | +### Syntax | ||
| 179 | + | ||
| 180 | +The examples are Golo's; the other eight languages map their own constructs onto the same classes. | ||
| 181 | + | ||
| 182 | +| Key | What it colours | | ||
| 183 | +| --- | --- | | ||
| 184 | +| `syntax.identifier` | An ordinary name — `args`, `item`, `this` | | ||
| 185 | +| `syntax.keyword` | `function`, `let`, `module`, `import`, `foreach`, `match`, `when`, … | | ||
| 186 | +| `syntax.type` | A capitalised name — `Point`, `Shape`, `Circle` — and the dotted path after `module` or `import` | | ||
| 187 | +| `syntax.builtin` | `println`, `str`, `len`, `list`, `map`, `range`, … | | ||
| 188 | +| `syntax.constant` | `true`, `false`, `null` | | ||
| 189 | +| `syntax.function` | A lower-case name before `(`, or the name after `function` | | ||
| 190 | +| `syntax.string` | `"…"` and `"""…"""` | | ||
| 191 | +| `syntax.char` | A `'c'` character literal | | ||
| 192 | +| `syntax.number` | `42`, `3.14`, `1e3`, `42L`, `2.0F` | | ||
| 193 | +| `syntax.comment` | `#` to the end of the line, and `----` … `----` | | ||
| 194 | +| `syntax.operator` | `+`, `->`, `==`, `?:`, `..`, … | | ||
| 195 | +| `syntax.punctuation` | Brackets, commas, dots, semicolons, and the `$` of `Shape$Circle` | | ||
| 196 | +| `syntax.heading` | A Markdown heading, whole line | | ||
| 197 | +| `syntax.tag` | An HTML element name and its brackets | | ||
| 198 | +| `syntax.attribute` | An HTML attribute's name | | ||
| 199 | +| `syntax.emphasis` | Markdown bold and italic | | ||
| 200 | +| `syntax.link` | A Markdown link or image | | ||
| 201 | + | ||
| 202 | +Which language produces which class is in [Languages coloured](languages.md). | ||
| 203 | + | ||
| 204 | +### Completion and diagnostics | ||
| 205 | + | ||
| 206 | +| Key | What it colours | | ||
| 207 | +| --- | --- | | ||
| 208 | +| `completion.frame` | The popup's frame | | ||
| 209 | +| `completion.item` | A suggestion | | ||
| 210 | +| `completion.selected` | The highlighted suggestion | | ||
| 211 | +| `completion.detail` | The kind tag beside a suggestion | | ||
| 212 | +| `diagnostic.error` | An error from the language server | | ||
| 213 | +| `diagnostic.warning` | A warning | | ||
| 214 | +| `diagnostic.info` | A note | | ||
| 215 | + | ||
| 216 | +## Example | ||
| 217 | + | ||
| 218 | +```toml | ||
| 219 | +name = "Mine" | ||
| 220 | +description = "Turbo Classic, with readable comments." | ||
| 221 | +inherits = "turbo-classic" | ||
| 222 | + | ||
| 223 | +[colors] | ||
| 224 | +"syntax.comment" = { fg = "#8a8a8a", italic = true } | ||
| 225 | +"syntax.string" = { fg = "#87d7af" } | ||
| 226 | +"editor.currentline" = { bg = "#00005f" } | ||
| 227 | +``` | ||
| 228 | + | ||
| 229 | +## Errors | ||
| 230 | + | ||
| 231 | +| Message | Cause | | ||
| 232 | +| --- | --- | | ||
| 233 | +| `theme: not found: "x"` | No `x.toml` in the user directory or among the embedded themes. | | ||
| 234 | +| `theme: not found: "…" is not a plain theme name` | The name contains `/`, `\` or `..`. | | ||
| 235 | +| `invalid TOML: …` | The file is not valid TOML. | | ||
| 236 | +| `colors."k": fg: unknown colour "…"` | The colour name is not recognised. | | ||
| 237 | +| `inherits: chain deeper than 16, probably a loop` | Two themes inherit from each other, directly or through others. | | ||
| 238 | +| `inherits "x": theme: not found` | The parent named does not exist. | | ||
added
docs/en/reference/versioning.md +153 -0 | new file mode 100644 | ||
| @@ -0,0 +1,153 @@ | ||
| 1 | +# Reference: the version number | |
| 2 | + | |
| 3 | +> Neutral description of where the version Turbo Golo reports comes from, and what each way of building it produces. | |
| 4 | + | |
| 5 | +## Where the number comes from | |
| 6 | + | |
| 7 | +Three sources, consulted in this order. The first that answers wins. | |
| 8 | + | |
| 9 | +| Order | Source | Set by | | |
| 10 | +| --- | --- | --- | | |
| 11 | +| 1 | Linker stamps | `make build`, `make install`, `scripts/install.sh`, `03-build-releases.sh` | | |
| 12 | +| 2 | Go build information | The Go tool, automatically | | |
| 13 | +| 3 | `unknown` | Nothing — the value reported when no source could name the build | | |
| 14 | + | |
| 15 | +There is **no version constant in the source**. A number written into a `.go` file has to be edited as part of releasing, and is wrong the moment someone forgets. | |
| 16 | + | |
| 17 | +## Linker stamps | |
| 18 | + | |
| 19 | +Three package-level variables in turbo-core's `version` package — the library every Turbo editor shares, so the paths name `turbo-core`, not this repository — set with `-ldflags -X`. | |
| 20 | + | |
| 21 | +| Variable | Filled from | Example | | |
| 22 | +| --- | --- | --- | | |
| 23 | +| `stamp` | `git describe --tags --dirty` | `v0.1.0-14-g88a4c38` | | |
| 24 | +| `commit` | `git rev-parse --short HEAD` | `88a4c38` | | |
| 25 | +| `built` | `date -u +%Y-%m-%dT%H:%M:%SZ` | `2026-09-14T18:04:05Z` | | |
| 26 | + | |
| 27 | +```sh | |
| 28 | +go build -ldflags "\ | |
| 29 | + -X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.1.0' \ | |
| 30 | + -X 'rickub.com/turbo-editors/turbo-core/version.commit=88a4c38' \ | |
| 31 | + -X 'rickub.com/turbo-editors/turbo-core/version.built=2026-09-14T18:04:05Z'" . | |
| 32 | +``` | |
| 33 | + | |
| 34 | +A leading `v` is dropped for display: the tag is `v0.1.0`, the About box says `0.1.0`. | |
| 35 | + | |
| 36 | +## Go build information | |
| 37 | + | |
| 38 | +Read from `runtime/debug.ReadBuildInfo()` when nothing was stamped. | |
| 39 | + | |
| 40 | +| Field read | Used for | | |
| 41 | +| --- | --- | | |
| 42 | +| `Main.Version` | The number, unless it is empty, `(devel)`, or a pseudo-version | | |
| 43 | +| `vcs.revision` | The commit, abbreviated to seven characters | | |
| 44 | +| `vcs.modified` | Whether `-dirty` is appended | | |
| 45 | + | |
| 46 | +`vcs.time` is **not** used. It records when the commit was made, not when the binary was linked, so reporting it as a build date would be wrong on every binary built later than its own commit. | |
| 47 | + | |
| 48 | +A **pseudo-version** — `v0.0.0-20260914120000-0123456789ab` for a module with no tag yet, `v0.1.1-0.20260914120000-0123456789ab` for a commit after one — is the Go tool naming a commit that no tag names. It is reported as `devel`, not shown as written: its `0.1.1` is a patch release that does not exist. | |
| 49 | + | |
| 50 | +## What each build reports | |
| 51 | + | |
| 52 | +| Built by | Number | Commit | Built | | |
| 53 | +| --- | --- | --- | --- | | |
| 54 | +| `make build`, `make install`, `scripts/install.sh` | `0.1.0-14-g88a4c38` | yes | yes | | |
| 55 | +| The same, on a tagged commit | `0.1.0` | yes | yes | | |
| 56 | +| The same, with uncommitted changes | `0.1.0-14-g88a4c38-dirty` | yes | yes | | |
| 57 | +| The same, in a checkout with no tag at all | `devel` | yes | yes | | |
| 58 | +| `03-build-releases.sh` | the `TAG` in `release.env`, whatever `git describe` says | yes | yes | | |
| 59 | +| `go install rickub.com/turbo-editors/turbo-golo@v0.1.0` | `0.1.0` | no | no | | |
| 60 | +| `go install rickub.com/turbo-editors/turbo-golo@latest`, before any tag exists | `devel` | no | no | | |
| 61 | +| `go build .` in a checkout | `devel` | yes | no | | |
| 62 | +| `go build .` in a checkout with uncommitted changes | `devel-dirty` | yes | no | | |
| 63 | +| `go run .` | `unknown` | no | no | | |
| 64 | +| A checkout with no git, and no stamps | `unknown` | no | no | | |
| 65 | + | |
| 66 | +Only the stamped rows can report a tag: the Go build system does not read git tags. Until Turbo Golo's first tag exists, `git describe` answers nothing and the Makefile stamps `devel` — a stamped `devel` still carries the commit and the build date. | |
| 67 | + | |
| 68 | +## Checked at build time | |
| 69 | + | |
| 70 | +A linker stamp is a string, and a wrong one is not an error. `-X` naming a symbol that does not exist links happily and stamps nothing; the binary then falls back to Go build information and reports a version the build never meant — often `devel`, on a binary attached to a release. Nothing but running the binary catches it, so every build that produces one runs it. | |
| 71 | + | |
| 72 | +`scripts/check-version.sh` is what runs. | |
| 73 | + | |
| 74 | +| Called by | On | A failure fails | | |
| 75 | +| --- | --- | --- | | |
| 76 | +| `make build` | `bin/turbo-golo`, with `$(VERSION)` and `$(COMMIT)` | the build | | |
| 77 | +| `scripts/install.sh` | the staged binary, **before** it is installed | the install, leaving the binary already there untouched | | |
| 78 | +| `03-build-releases.sh` | the one staged asset this machine can run, with the tag | the release build | | |
| 79 | + | |
| 80 | +```sh | |
| 81 | +scripts/check-version.sh bin/turbo-golo v0.1.0 88a4c38 # a stamped build | |
| 82 | +scripts/check-version.sh bin/turbo-golo # nothing to expect | |
| 83 | +``` | |
| 84 | + | |
| 85 | +| Arguments | Passes when | | |
| 86 | +| --- | --- | | |
| 87 | +| binary, version, commit | the reported number **equals** the version with its leading `v` dropped, and the commit appears in the output | | |
| 88 | +| binary, version | the number equals it | | |
| 89 | +| binary | the number is anything but `unknown` | | |
| 90 | + | |
| 91 | +The version comparison is an equality, not a search. `0.1.0` is a substring of `10.1.0`, and of a commit hash that happens to contain it; a stamp that is nearly right is exactly what this exists to catch. | |
| 92 | + | |
| 93 | +| Exit | Meaning | | |
| 94 | +| --- | --- | | |
| 95 | +| `0` | The binary reports what the build meant. The line it printed is echoed. | | |
| 96 | +| `1` | It does not run, is not there, or reports something else. | | |
| 97 | +| `2` | No binary was named. | | |
| 98 | + | |
| 99 | +## Where it is shown | |
| 100 | + | |
| 101 | +### `-version` | |
| 102 | + | |
| 103 | +One line, carrying every part that is known. | |
| 104 | + | |
| 105 | +``` | |
| 106 | +Turbo Golo 0.1.0 (88a4c38, built 2026-09-14T18:04:05Z) | |
| 107 | +Turbo Golo 0.1.0 (88a4c38) | |
| 108 | +Turbo Golo 0.1.0 | |
| 109 | +``` | |
| 110 | + | |
| 111 | +### Help ▸ About | |
| 112 | + | |
| 113 | +One line per known fact. A fact the build did not record has **no line**, rather than an empty one. | |
| 114 | + | |
| 115 | +``` | |
| 116 | +Turbo Golo 0.1.0 | |
| 117 | + | |
| 118 | +A Turbo C-style editor for Golo, | |
| 119 | +written in Go. | |
| 120 | + | |
| 121 | +Commit: 88a4c38 | |
| 122 | +Built: 2026-09-14 18:04 UTC | |
| 123 | +Theme: Turbo Classic | |
| 124 | +``` | |
| 125 | + | |
| 126 | +`Built` is rendered in UTC as `YYYY-MM-DD HH:MM UTC`. A stamp that is not valid RFC 3339 is shown exactly as it was given, rather than dropped. | |
| 127 | + | |
| 128 | +### `make version` | |
| 129 | + | |
| 130 | +Prints what this checkout would stamp, without building. | |
| 131 | + | |
| 132 | +``` | |
| 133 | +$ make version | |
| 134 | +v0.1.0-14-g88a4c38 (88a4c38) | |
| 135 | +``` | |
| 136 | + | |
| 137 | +### `make ldflags` | |
| 138 | + | |
| 139 | +Prints the linker flags a stamped build uses, so a script can reuse them instead of repeating the `-X` paths. | |
| 140 | + | |
| 141 | +``` | |
| 142 | +$ make ldflags | |
| 143 | +-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.1.0' -X '….commit=7f8b36a' -X '….built=2026-09-14T19:02:03Z' | |
| 144 | +``` | |
| 145 | + | |
| 146 | +`03-build-releases.sh` reads it for its cross-compiles, overriding the version with the tag it is releasing — `make ldflags VERSION=v0.1.0` — so the binaries say what the release says rather than what `git describe` says. A binary cross-compiled without it reports `devel`, whatever the release it is attached to says. | |
| 147 | + | |
| 148 | +## See also | |
| 149 | + | |
| 150 | +- Cutting a release so the number is right: [How to make a release](../how-to/make-a-release.md) | |
| 151 | +- What `-version` is **not** for: it is written for a person. A script that needs the number should compare with `grep -F`, or ask git, rather than reading a field out of it. | |
| 152 | +- Why there is no version constant: [Design decisions](../explanation/design-decisions.md#the-version-is-a-property-of-the-build-not-of-the-source) | |
| 153 | +- The `-version` flag among the others: [Command line](cli.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,153 @@ | |||
| 1 | +# Reference: the version number | ||
| 2 | + | ||
| 3 | +> Neutral description of where the version Turbo Golo reports comes from, and what each way of building it produces. | ||
| 4 | + | ||
| 5 | +## Where the number comes from | ||
| 6 | + | ||
| 7 | +Three sources, consulted in this order. The first that answers wins. | ||
| 8 | + | ||
| 9 | +| Order | Source | Set by | | ||
| 10 | +| --- | --- | --- | | ||
| 11 | +| 1 | Linker stamps | `make build`, `make install`, `scripts/install.sh`, `03-build-releases.sh` | | ||
| 12 | +| 2 | Go build information | The Go tool, automatically | | ||
| 13 | +| 3 | `unknown` | Nothing — the value reported when no source could name the build | | ||
| 14 | + | ||
| 15 | +There is **no version constant in the source**. A number written into a `.go` file has to be edited as part of releasing, and is wrong the moment someone forgets. | ||
| 16 | + | ||
| 17 | +## Linker stamps | ||
| 18 | + | ||
| 19 | +Three package-level variables in turbo-core's `version` package — the library every Turbo editor shares, so the paths name `turbo-core`, not this repository — set with `-ldflags -X`. | ||
| 20 | + | ||
| 21 | +| Variable | Filled from | Example | | ||
| 22 | +| --- | --- | --- | | ||
| 23 | +| `stamp` | `git describe --tags --dirty` | `v0.1.0-14-g88a4c38` | | ||
| 24 | +| `commit` | `git rev-parse --short HEAD` | `88a4c38` | | ||
| 25 | +| `built` | `date -u +%Y-%m-%dT%H:%M:%SZ` | `2026-09-14T18:04:05Z` | | ||
| 26 | + | ||
| 27 | +```sh | ||
| 28 | +go build -ldflags "\ | ||
| 29 | + -X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.1.0' \ | ||
| 30 | + -X 'rickub.com/turbo-editors/turbo-core/version.commit=88a4c38' \ | ||
| 31 | + -X 'rickub.com/turbo-editors/turbo-core/version.built=2026-09-14T18:04:05Z'" . | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +A leading `v` is dropped for display: the tag is `v0.1.0`, the About box says `0.1.0`. | ||
| 35 | + | ||
| 36 | +## Go build information | ||
| 37 | + | ||
| 38 | +Read from `runtime/debug.ReadBuildInfo()` when nothing was stamped. | ||
| 39 | + | ||
| 40 | +| Field read | Used for | | ||
| 41 | +| --- | --- | | ||
| 42 | +| `Main.Version` | The number, unless it is empty, `(devel)`, or a pseudo-version | | ||
| 43 | +| `vcs.revision` | The commit, abbreviated to seven characters | | ||
| 44 | +| `vcs.modified` | Whether `-dirty` is appended | | ||
| 45 | + | ||
| 46 | +`vcs.time` is **not** used. It records when the commit was made, not when the binary was linked, so reporting it as a build date would be wrong on every binary built later than its own commit. | ||
| 47 | + | ||
| 48 | +A **pseudo-version** — `v0.0.0-20260914120000-0123456789ab` for a module with no tag yet, `v0.1.1-0.20260914120000-0123456789ab` for a commit after one — is the Go tool naming a commit that no tag names. It is reported as `devel`, not shown as written: its `0.1.1` is a patch release that does not exist. | ||
| 49 | + | ||
| 50 | +## What each build reports | ||
| 51 | + | ||
| 52 | +| Built by | Number | Commit | Built | | ||
| 53 | +| --- | --- | --- | --- | | ||
| 54 | +| `make build`, `make install`, `scripts/install.sh` | `0.1.0-14-g88a4c38` | yes | yes | | ||
| 55 | +| The same, on a tagged commit | `0.1.0` | yes | yes | | ||
| 56 | +| The same, with uncommitted changes | `0.1.0-14-g88a4c38-dirty` | yes | yes | | ||
| 57 | +| The same, in a checkout with no tag at all | `devel` | yes | yes | | ||
| 58 | +| `03-build-releases.sh` | the `TAG` in `release.env`, whatever `git describe` says | yes | yes | | ||
| 59 | +| `go install rickub.com/turbo-editors/turbo-golo@v0.1.0` | `0.1.0` | no | no | | ||
| 60 | +| `go install rickub.com/turbo-editors/turbo-golo@latest`, before any tag exists | `devel` | no | no | | ||
| 61 | +| `go build .` in a checkout | `devel` | yes | no | | ||
| 62 | +| `go build .` in a checkout with uncommitted changes | `devel-dirty` | yes | no | | ||
| 63 | +| `go run .` | `unknown` | no | no | | ||
| 64 | +| A checkout with no git, and no stamps | `unknown` | no | no | | ||
| 65 | + | ||
| 66 | +Only the stamped rows can report a tag: the Go build system does not read git tags. Until Turbo Golo's first tag exists, `git describe` answers nothing and the Makefile stamps `devel` — a stamped `devel` still carries the commit and the build date. | ||
| 67 | + | ||
| 68 | +## Checked at build time | ||
| 69 | + | ||
| 70 | +A linker stamp is a string, and a wrong one is not an error. `-X` naming a symbol that does not exist links happily and stamps nothing; the binary then falls back to Go build information and reports a version the build never meant — often `devel`, on a binary attached to a release. Nothing but running the binary catches it, so every build that produces one runs it. | ||
| 71 | + | ||
| 72 | +`scripts/check-version.sh` is what runs. | ||
| 73 | + | ||
| 74 | +| Called by | On | A failure fails | | ||
| 75 | +| --- | --- | --- | | ||
| 76 | +| `make build` | `bin/turbo-golo`, with `$(VERSION)` and `$(COMMIT)` | the build | | ||
| 77 | +| `scripts/install.sh` | the staged binary, **before** it is installed | the install, leaving the binary already there untouched | | ||
| 78 | +| `03-build-releases.sh` | the one staged asset this machine can run, with the tag | the release build | | ||
| 79 | + | ||
| 80 | +```sh | ||
| 81 | +scripts/check-version.sh bin/turbo-golo v0.1.0 88a4c38 # a stamped build | ||
| 82 | +scripts/check-version.sh bin/turbo-golo # nothing to expect | ||
| 83 | +``` | ||
| 84 | + | ||
| 85 | +| Arguments | Passes when | | ||
| 86 | +| --- | --- | | ||
| 87 | +| binary, version, commit | the reported number **equals** the version with its leading `v` dropped, and the commit appears in the output | | ||
| 88 | +| binary, version | the number equals it | | ||
| 89 | +| binary | the number is anything but `unknown` | | ||
| 90 | + | ||
| 91 | +The version comparison is an equality, not a search. `0.1.0` is a substring of `10.1.0`, and of a commit hash that happens to contain it; a stamp that is nearly right is exactly what this exists to catch. | ||
| 92 | + | ||
| 93 | +| Exit | Meaning | | ||
| 94 | +| --- | --- | | ||
| 95 | +| `0` | The binary reports what the build meant. The line it printed is echoed. | | ||
| 96 | +| `1` | It does not run, is not there, or reports something else. | | ||
| 97 | +| `2` | No binary was named. | | ||
| 98 | + | ||
| 99 | +## Where it is shown | ||
| 100 | + | ||
| 101 | +### `-version` | ||
| 102 | + | ||
| 103 | +One line, carrying every part that is known. | ||
| 104 | + | ||
| 105 | +``` | ||
| 106 | +Turbo Golo 0.1.0 (88a4c38, built 2026-09-14T18:04:05Z) | ||
| 107 | +Turbo Golo 0.1.0 (88a4c38) | ||
| 108 | +Turbo Golo 0.1.0 | ||
| 109 | +``` | ||
| 110 | + | ||
| 111 | +### Help ▸ About | ||
| 112 | + | ||
| 113 | +One line per known fact. A fact the build did not record has **no line**, rather than an empty one. | ||
| 114 | + | ||
| 115 | +``` | ||
| 116 | +Turbo Golo 0.1.0 | ||
| 117 | + | ||
| 118 | +A Turbo C-style editor for Golo, | ||
| 119 | +written in Go. | ||
| 120 | + | ||
| 121 | +Commit: 88a4c38 | ||
| 122 | +Built: 2026-09-14 18:04 UTC | ||
| 123 | +Theme: Turbo Classic | ||
| 124 | +``` | ||
| 125 | + | ||
| 126 | +`Built` is rendered in UTC as `YYYY-MM-DD HH:MM UTC`. A stamp that is not valid RFC 3339 is shown exactly as it was given, rather than dropped. | ||
| 127 | + | ||
| 128 | +### `make version` | ||
| 129 | + | ||
| 130 | +Prints what this checkout would stamp, without building. | ||
| 131 | + | ||
| 132 | +``` | ||
| 133 | +$ make version | ||
| 134 | +v0.1.0-14-g88a4c38 (88a4c38) | ||
| 135 | +``` | ||
| 136 | + | ||
| 137 | +### `make ldflags` | ||
| 138 | + | ||
| 139 | +Prints the linker flags a stamped build uses, so a script can reuse them instead of repeating the `-X` paths. | ||
| 140 | + | ||
| 141 | +``` | ||
| 142 | +$ make ldflags | ||
| 143 | +-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.1.0' -X '….commit=7f8b36a' -X '….built=2026-09-14T19:02:03Z' | ||
| 144 | +``` | ||
| 145 | + | ||
| 146 | +`03-build-releases.sh` reads it for its cross-compiles, overriding the version with the tag it is releasing — `make ldflags VERSION=v0.1.0` — so the binaries say what the release says rather than what `git describe` says. A binary cross-compiled without it reports `devel`, whatever the release it is attached to says. | ||
| 147 | + | ||
| 148 | +## See also | ||
| 149 | + | ||
| 150 | +- Cutting a release so the number is right: [How to make a release](../how-to/make-a-release.md) | ||
| 151 | +- What `-version` is **not** for: it is written for a person. A script that needs the number should compare with `grep -F`, or ask git, rather than reading a field out of it. | ||
| 152 | +- Why there is no version constant: [Design decisions](../explanation/design-decisions.md#the-version-is-a-property-of-the-build-not-of-the-source) | ||
| 153 | +- The `-version` flag among the others: [Command line](cli.md) | ||
added
docs/en/tutorials/getting-started.md +204 -0 | new file mode 100644 | ||
| @@ -0,0 +1,204 @@ | ||
| 1 | +# Tutorial: your first Golo program in Turbo Golo | |
| 2 | + | |
| 3 | +By the end of this tutorial you will have written, run and broken a small Golo program without leaving the editor — and seen the editor tell you where the mistake was. | |
| 4 | + | |
| 5 | +No prior knowledge of Turbo Golo is needed. You need Go 1.26 or later to build the editor, and the `golo` interpreter to run the program. | |
| 6 | + | |
| 7 | +## Prerequisites | |
| 8 | + | |
| 9 | +Check Go: | |
| 10 | + | |
| 11 | +```bash | |
| 12 | +go version | |
| 13 | +``` | |
| 14 | + | |
| 15 | +You should see something like: | |
| 16 | + | |
| 17 | +``` | |
| 18 | +go version go1.26.5 linux/arm64 | |
| 19 | +``` | |
| 20 | + | |
| 21 | +Check the interpreter: | |
| 22 | + | |
| 23 | +```bash | |
| 24 | +golo --version | |
| 25 | +``` | |
| 26 | + | |
| 27 | +``` | |
| 28 | +v0.1.1 | dev.20260802.🤓 | |
| 29 | +``` | |
| 30 | + | |
| 31 | +If that command is not found, install it first — [a download or one script does it](../how-to/install-goloscript.md). | |
| 32 | + | |
| 33 | +## Step 1 — Install the editor | |
| 34 | + | |
| 35 | +```bash | |
| 36 | +git clone https://rickub.com/turbo-editors/turbo-golo.git | |
| 37 | +cd turbo-golo | |
| 38 | +make install | |
| 39 | +``` | |
| 40 | + | |
| 41 | +The installer builds, installs, and then checks what it installed. The last lines are: | |
| 42 | + | |
| 43 | +``` | |
| 44 | +==> Checking the language server | |
| 45 | + ✓ golo at /usr/local/bin/golo — completion comes from `golo lsp` | |
| 46 | + | |
| 47 | +==> Ready | |
| 48 | +``` | |
| 49 | + | |
| 50 | +We now have a `turbo-golo` command. | |
| 51 | + | |
| 52 | +## Step 2 — Make a directory | |
| 53 | + | |
| 54 | +```bash | |
| 55 | +mkdir /tmp/hello | |
| 56 | +cd /tmp/hello | |
| 57 | +``` | |
| 58 | + | |
| 59 | +That is the whole of making a Golo project: Golo has no manifest, a script is a file, and a program is a directory of them. **The directory you start the editor in** is where the Golo menu's commands will run, so start it here. | |
| 60 | + | |
| 61 | +## Step 3 — Open a file that does not exist yet | |
| 62 | + | |
| 63 | +```bash | |
| 64 | +turbo-golo hello.golo | |
| 65 | +``` | |
| 66 | + | |
| 67 | +The screen fills. Along the top: | |
| 68 | + | |
| 69 | +``` | |
| 70 | + File Edit Search Run Code Options Window Snippets Golo Help | |
| 71 | +``` | |
| 72 | + | |
| 73 | +Ten menus, and the ninth is named after the language. In the middle, an empty window titled `hello.golo`. Along the bottom, at the right-hand end, you should see: | |
| 74 | + | |
| 75 | +``` | |
| 76 | +1:1 LSP: ready | |
| 77 | +``` | |
| 78 | + | |
| 79 | +`LSP: ready` means `golo lsp` — the interpreter, in language-server mode — started in this directory. We will use it in Step 8. | |
| 80 | + | |
| 81 | +## Step 4 — Write the program | |
| 82 | + | |
| 83 | +Type this in. Type it exactly; we will look at the colours next. | |
| 84 | + | |
| 85 | +```golo | |
| 86 | +module hello.World | |
| 87 | + | |
| 88 | +# Greet someone several times | |
| 89 | +struct Greeting = { name, times } | |
| 90 | + | |
| 91 | +function greet = |g| { | |
| 92 | + foreach i in range(1, g: times() + 1) { | |
| 93 | + println("Hello, " + g: name() + "! (" + i + ")") | |
| 94 | + } | |
| 95 | +} | |
| 96 | + | |
| 97 | +function main = |args| { | |
| 98 | + greet(Greeting("Golo", 3)) | |
| 99 | +} | |
| 100 | +``` | |
| 101 | + | |
| 102 | +If a completion list drops down while you type — a `.` asks for one by itself, and `hello.` is one — keep typing or press `Escape`; `Enter` would accept the first entry. | |
| 103 | + | |
| 104 | +Press `F2` to save. The status bar says `Saved hello.golo`. | |
| 105 | + | |
| 106 | +## Step 5 — Read the colours | |
| 107 | + | |
| 108 | +Look at what you have typed. In the default `turbo-classic` theme: | |
| 109 | + | |
| 110 | +| What | Colour | | |
| 111 | +| --- | --- | | |
| 112 | +| `module`, `struct`, `function`, `foreach`, `in` | bright white, bold — keywords | | |
| 113 | +| `hello.World`, `Greeting` | bright cyan — a module path, and a type | | |
| 114 | +| `greet`, where it is declared and where it is called | bright yellow, bold — functions | | |
| 115 | +| `range`, `println` | bright cyan, bold — functions the interpreter provides | | |
| 116 | +| `name`, `times`, `g`, `i`, `args` | bright yellow — ordinary names | | |
| 117 | +| `"Hello, "`, `"! ("`, `")"`, `"Golo"` | green — strings | | |
| 118 | +| `1`, `3` | magenta — numbers | | |
| 119 | +| `# Greet someone several times` | grey — a comment | | |
| 120 | +| `\|`, `:`, `+`, `=` | white — operators | | |
| 121 | + | |
| 122 | +Two of those rows are worth a second look. | |
| 123 | + | |
| 124 | +**`Greeting` is cyan and `greet` is yellow**, and nothing in the editor was told which of them is a type. Golo's convention decides it: structs, unions and their variants are the names people capitalise, so a capital letter is coloured as a type. | |
| 125 | + | |
| 126 | +**`greet` is yellow where it is declared**, after `function`, although no parenthesis follows it there. The name after `function` is a function by position — [and that is a deliberate rule](../explanation/colouring-and-completion.md). | |
| 127 | + | |
| 128 | +## Step 6 — Give the project its tools | |
| 129 | + | |
| 130 | +Press `F10` to open the menu bar, then `→` **eight times** to reach **Golo** — past Edit, Search, Run, Code, Options, Window and Snippets. Faster: press `Alt-G`. | |
| 131 | + | |
| 132 | +The menu holds two items, and only one of them is available: | |
| 133 | + | |
| 134 | +``` | |
| 135 | +┌───────────────────┐ | |
| 136 | +│ Create tools file │ | |
| 137 | +│ Open tools file │ ← greyed out; there is no file to open yet | |
| 138 | +└───────────────────┘ | |
| 139 | +``` | |
| 140 | + | |
| 141 | +Choose **Create tools file**. | |
| 142 | + | |
| 143 | +A second window opens on the file that was just written, `.turbo-golo/tools.toml`. Read it if you like — it explains every key it uses — then press `Ctrl-W` to close it. | |
| 144 | + | |
| 145 | +Look at the menu bar: a **Tools** menu has appeared between Golo and Help. The starter file's last entry names a menu of its own, and that one line is the whole mechanism. | |
| 146 | + | |
| 147 | +Open the Golo menu again. It now holds eight commands, and the two items have swapped: `Create tools file` is greyed out, and `Open tools file` is the one you can choose. | |
| 148 | + | |
| 149 | +## Step 7 — Run it | |
| 150 | + | |
| 151 | +Press `Alt-G` and choose **Run**. A box asks for a value before the command runs, because Golo has no manifest that says which file is the program: | |
| 152 | + | |
| 153 | +``` | |
| 154 | +┌──────────────── Run ────────────────┐ | |
| 155 | +│ script, e.g. main.golo │ | |
| 156 | +│ [ ] │ | |
| 157 | +└─────────────────────────────────────┘ | |
| 158 | +``` | |
| 159 | + | |
| 160 | +Type `hello.golo` and press `Enter`. A terminal window opens and the program runs in it: | |
| 161 | + | |
| 162 | +``` | |
| 163 | +Hello, Golo! (1) | |
| 164 | +Hello, Golo! (2) | |
| 165 | +Hello, Golo! (3) | |
| 166 | +``` | |
| 167 | + | |
| 168 | +A terminal rather than a dialog, because a program that reads the keyboard has to be answerable. The program has finished, so the window has stopped behaving like a terminal and every key reaches the editor again: press `Ctrl-W` to close it. | |
| 169 | + | |
| 170 | +## Step 8 — Break it, and see where | |
| 171 | + | |
| 172 | +Go to the end of the file, after the last `}`, and add a comment the way another language would write it: | |
| 173 | + | |
| 174 | +```golo | |
| 175 | +// run it | |
| 176 | +``` | |
| 177 | + | |
| 178 | +Press `F2` to save. | |
| 179 | + | |
| 180 | +Within a second, two things happen. A red `×` appears in the gutter, just left of that line's number. And the status bar reads: | |
| 181 | + | |
| 182 | +``` | |
| 183 | +⚠ GoloScript has no '//' line comments. GoloScript comments are '#' for a single line (e.g. … | |
| 184 | +``` | |
| 185 | + | |
| 186 | +Nothing asked for that. `golo lsp` publishes it by itself whenever it re-reads the file — a syntax error would have earned the same mark, and this one is a lint the server adds because it is the mistake everybody arriving from another language makes first. | |
| 187 | + | |
| 188 | +Change the `//` to `#` and save again; both the mark and the message go away. | |
| 189 | + | |
| 190 | +## Step 9 — Change the theme | |
| 191 | + | |
| 192 | +`F10`, then `→` **five times** to reach **Options** — past Edit, Search, Run and Code. Choose **Theme…**. | |
| 193 | + | |
| 194 | +A list opens on the theme you are in. Press `↓` to `cobalt` and `Enter`. The whole screen changes, keeping the same shape. | |
| 195 | + | |
| 196 | +Press `Alt-X` to leave. The editor asks about unsaved files first, if there are any. | |
| 197 | + | |
| 198 | +## What now? | |
| 199 | + | |
| 200 | +You have written a Golo program in the editor, run it, broken it, and seen the editor say where. To go further: | |
| 201 | + | |
| 202 | +- To do specific things → the [how-to guides](../how-to/) | |
| 203 | +- To see exactly what is coloured and how → [languages coloured](../reference/languages.md) | |
| 204 | +- To understand why the editor is built this way → the [explanation](../explanation/) | |
| new file mode 100644 | |||
| @@ -0,0 +1,204 @@ | |||
| 1 | +# Tutorial: your first Golo program in Turbo Golo | ||
| 2 | + | ||
| 3 | +By the end of this tutorial you will have written, run and broken a small Golo program without leaving the editor — and seen the editor tell you where the mistake was. | ||
| 4 | + | ||
| 5 | +No prior knowledge of Turbo Golo is needed. You need Go 1.26 or later to build the editor, and the `golo` interpreter to run the program. | ||
| 6 | + | ||
| 7 | +## Prerequisites | ||
| 8 | + | ||
| 9 | +Check Go: | ||
| 10 | + | ||
| 11 | +```bash | ||
| 12 | +go version | ||
| 13 | +``` | ||
| 14 | + | ||
| 15 | +You should see something like: | ||
| 16 | + | ||
| 17 | +``` | ||
| 18 | +go version go1.26.5 linux/arm64 | ||
| 19 | +``` | ||
| 20 | + | ||
| 21 | +Check the interpreter: | ||
| 22 | + | ||
| 23 | +```bash | ||
| 24 | +golo --version | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +``` | ||
| 28 | +v0.1.1 | dev.20260802.🤓 | ||
| 29 | +``` | ||
| 30 | + | ||
| 31 | +If that command is not found, install it first — [a download or one script does it](../how-to/install-goloscript.md). | ||
| 32 | + | ||
| 33 | +## Step 1 — Install the editor | ||
| 34 | + | ||
| 35 | +```bash | ||
| 36 | +git clone https://rickub.com/turbo-editors/turbo-golo.git | ||
| 37 | +cd turbo-golo | ||
| 38 | +make install | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +The installer builds, installs, and then checks what it installed. The last lines are: | ||
| 42 | + | ||
| 43 | +``` | ||
| 44 | +==> Checking the language server | ||
| 45 | + ✓ golo at /usr/local/bin/golo — completion comes from `golo lsp` | ||
| 46 | + | ||
| 47 | +==> Ready | ||
| 48 | +``` | ||
| 49 | + | ||
| 50 | +We now have a `turbo-golo` command. | ||
| 51 | + | ||
| 52 | +## Step 2 — Make a directory | ||
| 53 | + | ||
| 54 | +```bash | ||
| 55 | +mkdir /tmp/hello | ||
| 56 | +cd /tmp/hello | ||
| 57 | +``` | ||
| 58 | + | ||
| 59 | +That is the whole of making a Golo project: Golo has no manifest, a script is a file, and a program is a directory of them. **The directory you start the editor in** is where the Golo menu's commands will run, so start it here. | ||
| 60 | + | ||
| 61 | +## Step 3 — Open a file that does not exist yet | ||
| 62 | + | ||
| 63 | +```bash | ||
| 64 | +turbo-golo hello.golo | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +The screen fills. Along the top: | ||
| 68 | + | ||
| 69 | +``` | ||
| 70 | + File Edit Search Run Code Options Window Snippets Golo Help | ||
| 71 | +``` | ||
| 72 | + | ||
| 73 | +Ten menus, and the ninth is named after the language. In the middle, an empty window titled `hello.golo`. Along the bottom, at the right-hand end, you should see: | ||
| 74 | + | ||
| 75 | +``` | ||
| 76 | +1:1 LSP: ready | ||
| 77 | +``` | ||
| 78 | + | ||
| 79 | +`LSP: ready` means `golo lsp` — the interpreter, in language-server mode — started in this directory. We will use it in Step 8. | ||
| 80 | + | ||
| 81 | +## Step 4 — Write the program | ||
| 82 | + | ||
| 83 | +Type this in. Type it exactly; we will look at the colours next. | ||
| 84 | + | ||
| 85 | +```golo | ||
| 86 | +module hello.World | ||
| 87 | + | ||
| 88 | +# Greet someone several times | ||
| 89 | +struct Greeting = { name, times } | ||
| 90 | + | ||
| 91 | +function greet = |g| { | ||
| 92 | + foreach i in range(1, g: times() + 1) { | ||
| 93 | + println("Hello, " + g: name() + "! (" + i + ")") | ||
| 94 | + } | ||
| 95 | +} | ||
| 96 | + | ||
| 97 | +function main = |args| { | ||
| 98 | + greet(Greeting("Golo", 3)) | ||
| 99 | +} | ||
| 100 | +``` | ||
| 101 | + | ||
| 102 | +If a completion list drops down while you type — a `.` asks for one by itself, and `hello.` is one — keep typing or press `Escape`; `Enter` would accept the first entry. | ||
| 103 | + | ||
| 104 | +Press `F2` to save. The status bar says `Saved hello.golo`. | ||
| 105 | + | ||
| 106 | +## Step 5 — Read the colours | ||
| 107 | + | ||
| 108 | +Look at what you have typed. In the default `turbo-classic` theme: | ||
| 109 | + | ||
| 110 | +| What | Colour | | ||
| 111 | +| --- | --- | | ||
| 112 | +| `module`, `struct`, `function`, `foreach`, `in` | bright white, bold — keywords | | ||
| 113 | +| `hello.World`, `Greeting` | bright cyan — a module path, and a type | | ||
| 114 | +| `greet`, where it is declared and where it is called | bright yellow, bold — functions | | ||
| 115 | +| `range`, `println` | bright cyan, bold — functions the interpreter provides | | ||
| 116 | +| `name`, `times`, `g`, `i`, `args` | bright yellow — ordinary names | | ||
| 117 | +| `"Hello, "`, `"! ("`, `")"`, `"Golo"` | green — strings | | ||
| 118 | +| `1`, `3` | magenta — numbers | | ||
| 119 | +| `# Greet someone several times` | grey — a comment | | ||
| 120 | +| `\|`, `:`, `+`, `=` | white — operators | | ||
| 121 | + | ||
| 122 | +Two of those rows are worth a second look. | ||
| 123 | + | ||
| 124 | +**`Greeting` is cyan and `greet` is yellow**, and nothing in the editor was told which of them is a type. Golo's convention decides it: structs, unions and their variants are the names people capitalise, so a capital letter is coloured as a type. | ||
| 125 | + | ||
| 126 | +**`greet` is yellow where it is declared**, after `function`, although no parenthesis follows it there. The name after `function` is a function by position — [and that is a deliberate rule](../explanation/colouring-and-completion.md). | ||
| 127 | + | ||
| 128 | +## Step 6 — Give the project its tools | ||
| 129 | + | ||
| 130 | +Press `F10` to open the menu bar, then `→` **eight times** to reach **Golo** — past Edit, Search, Run, Code, Options, Window and Snippets. Faster: press `Alt-G`. | ||
| 131 | + | ||
| 132 | +The menu holds two items, and only one of them is available: | ||
| 133 | + | ||
| 134 | +``` | ||
| 135 | +┌───────────────────┐ | ||
| 136 | +│ Create tools file │ | ||
| 137 | +│ Open tools file │ ← greyed out; there is no file to open yet | ||
| 138 | +└───────────────────┘ | ||
| 139 | +``` | ||
| 140 | + | ||
| 141 | +Choose **Create tools file**. | ||
| 142 | + | ||
| 143 | +A second window opens on the file that was just written, `.turbo-golo/tools.toml`. Read it if you like — it explains every key it uses — then press `Ctrl-W` to close it. | ||
| 144 | + | ||
| 145 | +Look at the menu bar: a **Tools** menu has appeared between Golo and Help. The starter file's last entry names a menu of its own, and that one line is the whole mechanism. | ||
| 146 | + | ||
| 147 | +Open the Golo menu again. It now holds eight commands, and the two items have swapped: `Create tools file` is greyed out, and `Open tools file` is the one you can choose. | ||
| 148 | + | ||
| 149 | +## Step 7 — Run it | ||
| 150 | + | ||
| 151 | +Press `Alt-G` and choose **Run**. A box asks for a value before the command runs, because Golo has no manifest that says which file is the program: | ||
| 152 | + | ||
| 153 | +``` | ||
| 154 | +┌──────────────── Run ────────────────┐ | ||
| 155 | +│ script, e.g. main.golo │ | ||
| 156 | +│ [ ] │ | ||
| 157 | +└─────────────────────────────────────┘ | ||
| 158 | +``` | ||
| 159 | + | ||
| 160 | +Type `hello.golo` and press `Enter`. A terminal window opens and the program runs in it: | ||
| 161 | + | ||
| 162 | +``` | ||
| 163 | +Hello, Golo! (1) | ||
| 164 | +Hello, Golo! (2) | ||
| 165 | +Hello, Golo! (3) | ||
| 166 | +``` | ||
| 167 | + | ||
| 168 | +A terminal rather than a dialog, because a program that reads the keyboard has to be answerable. The program has finished, so the window has stopped behaving like a terminal and every key reaches the editor again: press `Ctrl-W` to close it. | ||
| 169 | + | ||
| 170 | +## Step 8 — Break it, and see where | ||
| 171 | + | ||
| 172 | +Go to the end of the file, after the last `}`, and add a comment the way another language would write it: | ||
| 173 | + | ||
| 174 | +```golo | ||
| 175 | +// run it | ||
| 176 | +``` | ||
| 177 | + | ||
| 178 | +Press `F2` to save. | ||
| 179 | + | ||
| 180 | +Within a second, two things happen. A red `×` appears in the gutter, just left of that line's number. And the status bar reads: | ||
| 181 | + | ||
| 182 | +``` | ||
| 183 | +⚠ GoloScript has no '//' line comments. GoloScript comments are '#' for a single line (e.g. … | ||
| 184 | +``` | ||
| 185 | + | ||
| 186 | +Nothing asked for that. `golo lsp` publishes it by itself whenever it re-reads the file — a syntax error would have earned the same mark, and this one is a lint the server adds because it is the mistake everybody arriving from another language makes first. | ||
| 187 | + | ||
| 188 | +Change the `//` to `#` and save again; both the mark and the message go away. | ||
| 189 | + | ||
| 190 | +## Step 9 — Change the theme | ||
| 191 | + | ||
| 192 | +`F10`, then `→` **five times** to reach **Options** — past Edit, Search, Run and Code. Choose **Theme…**. | ||
| 193 | + | ||
| 194 | +A list opens on the theme you are in. Press `↓` to `cobalt` and `Enter`. The whole screen changes, keeping the same shape. | ||
| 195 | + | ||
| 196 | +Press `Alt-X` to leave. The editor asks about unsaved files first, if there are any. | ||
| 197 | + | ||
| 198 | +## What now? | ||
| 199 | + | ||
| 200 | +You have written a Golo program in the editor, run it, broken it, and seen the editor say where. To go further: | ||
| 201 | + | ||
| 202 | +- To do specific things → the [how-to guides](../how-to/) | ||
| 203 | +- To see exactly what is coloured and how → [languages coloured](../reference/languages.md) | ||
| 204 | +- To understand why the editor is built this way → the [explanation](../explanation/) | ||
added
docs/fr/README.md +63 -0 | new file mode 100644 | ||
| @@ -0,0 +1,63 @@ | ||
| 1 | +# Turbo Golo — documentation | |
| 2 | + | |
| 3 | +Turbo Golo est un éditeur pour Golo dans le style de Turbo C : un IDE plein écran en terminal, avec menus, fenêtres déplaçables, coloration syntaxique pour neuf langages, thèmes, complétion via `golo lsp`, fenêtres shell, réglages par projet, arbre du projet, snippets, et la chaîne d'outils GoloScript à un menu de distance. | |
| 4 | + | |
| 5 | +Il est bâti sur [turbo-core](https://rickub.com/turbo-editors/turbo-core), la bibliothèque que partage chaque éditeur Turbo. Si vous voulez construire votre propre éditeur, c'est là qu'il faut regarder. | |
| 6 | + | |
| 7 | +Cette documentation suit la méthode [Diátaxis](https://diataxis.fr). Quatre types de page, quatre besoins différents — allez directement à celui qui correspond à ce que vous cherchez. | |
| 8 | + | |
| 9 | +| Je veux… | Aller à | | |
| 10 | +| --- | --- | | |
| 11 | +| **apprendre** l'éditeur en l'utilisant | [Tutoriels](tutorials/) | | |
| 12 | +| **faire** quelque chose de précis | [Guides pratiques](how-to/) | | |
| 13 | +| **consulter** un détail exact | [Référence](reference/) | | |
| 14 | +| **comprendre** comment et pourquoi ça marche | [Explications](explanation/) | | |
| 15 | + | |
| 16 | +## Tutoriels — apprendre en faisant | |
| 17 | + | |
| 18 | +- [Votre premier programme Golo dans Turbo Golo](tutorials/getting-started.md) — l'installer, écrire un script, l'exécuter, le casser et voir l'éditeur dire où. | |
| 19 | +- [Projets de démonstration](../../demos/) — trois projets Golo à ouvrir dans l'éditeur une fois qu'il est là : un petit, un avec des tests, et un tour de toutes les constructions que le scanner colore. | |
| 20 | + | |
| 21 | +## Guides pratiques — des recettes pour une tâche | |
| 22 | + | |
| 23 | +- [Installer et compiler Turbo Golo](how-to/install.md) | |
| 24 | +- [Installer GoloScript](how-to/install-goloscript.md) | |
| 25 | +- [Lancer les tests](how-to/run-the-tests.md) | |
| 26 | +- [Activer la complétion Golo](how-to/enable-completion.md) | |
| 27 | +- [Écrire son propre thème](how-to/write-a-theme.md) | |
| 28 | +- [Se déplacer dans un fichier](how-to/navigate-code.md) | |
| 29 | +- [Interroger le code](how-to/ask-about-code.md) | |
| 30 | +- [Lancer des commandes shell sans quitter l'éditeur](how-to/use-a-terminal.md) | |
| 31 | +- [Donner ses propres réglages à un projet](how-to/configure-a-project.md) | |
| 32 | +- [Parcourir un projet et ouvrir des fichiers depuis un arbre](how-to/browse-a-project.md) | |
| 33 | +- [Insérer des snippets depuis un menu](how-to/use-snippets.md) | |
| 34 | +- [Lancer les commandes golo depuis l'éditeur](how-to/run-golo-commands.md) | |
| 35 | +- [Faire une release](how-to/make-a-release.md) | |
| 36 | +- [Dialoguer avec un agent de code depuis l'éditeur](how-to/talk-to-an-agent.md) | |
| 37 | + | |
| 38 | +## Référence — les détails exacts | |
| 39 | + | |
| 40 | +- [Ligne de commande](reference/cli.md) | |
| 41 | +- [Clavier](reference/keyboard.md) | |
| 42 | +- [Menus](reference/menus.md) | |
| 43 | +- [Format des fichiers de thème](reference/themes.md) | |
| 44 | +- [Fenêtres terminal](reference/terminal.md) | |
| 45 | +- [Réglages de projet](reference/project-settings.md) | |
| 46 | +- [Arbre du projet](reference/project-tree.md) | |
| 47 | +- [Langages colorés](reference/languages.md) | |
| 48 | +- [Snippets](reference/snippets.md) | |
| 49 | +- [Outils Golo](reference/golo-tools.md) | |
| 50 | +- [Le numéro de version](reference/versioning.md) | |
| 51 | +- [Agents et ACP](reference/acp.md) | |
| 52 | + | |
| 53 | +## Explications — comprendre | |
| 54 | + | |
| 55 | +- [Architecture](explanation/architecture.md) | |
| 56 | +- [Décisions de conception](explanation/design-decisions.md) | |
| 57 | +- [Coloration et complétion](explanation/colouring-and-completion.md) | |
| 58 | +- [Fenêtres terminal](explanation/terminal-windows.md) | |
| 59 | +- [Réglages de projet](explanation/project-settings.md) | |
| 60 | +- [Arbre du projet](explanation/project-tree.md) | |
| 61 | +- [Snippets](explanation/snippets.md) | |
| 62 | +- [Outils Golo](explanation/golo-tools.md) | |
| 63 | +- [Fenêtres agent](explanation/agent-windows.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | +# Turbo Golo — documentation | ||
| 2 | + | ||
| 3 | +Turbo Golo est un éditeur pour Golo dans le style de Turbo C : un IDE plein écran en terminal, avec menus, fenêtres déplaçables, coloration syntaxique pour neuf langages, thèmes, complétion via `golo lsp`, fenêtres shell, réglages par projet, arbre du projet, snippets, et la chaîne d'outils GoloScript à un menu de distance. | ||
| 4 | + | ||
| 5 | +Il est bâti sur [turbo-core](https://rickub.com/turbo-editors/turbo-core), la bibliothèque que partage chaque éditeur Turbo. Si vous voulez construire votre propre éditeur, c'est là qu'il faut regarder. | ||
| 6 | + | ||
| 7 | +Cette documentation suit la méthode [Diátaxis](https://diataxis.fr). Quatre types de page, quatre besoins différents — allez directement à celui qui correspond à ce que vous cherchez. | ||
| 8 | + | ||
| 9 | +| Je veux… | Aller à | | ||
| 10 | +| --- | --- | | ||
| 11 | +| **apprendre** l'éditeur en l'utilisant | [Tutoriels](tutorials/) | | ||
| 12 | +| **faire** quelque chose de précis | [Guides pratiques](how-to/) | | ||
| 13 | +| **consulter** un détail exact | [Référence](reference/) | | ||
| 14 | +| **comprendre** comment et pourquoi ça marche | [Explications](explanation/) | | ||
| 15 | + | ||
| 16 | +## Tutoriels — apprendre en faisant | ||
| 17 | + | ||
| 18 | +- [Votre premier programme Golo dans Turbo Golo](tutorials/getting-started.md) — l'installer, écrire un script, l'exécuter, le casser et voir l'éditeur dire où. | ||
| 19 | +- [Projets de démonstration](../../demos/) — trois projets Golo à ouvrir dans l'éditeur une fois qu'il est là : un petit, un avec des tests, et un tour de toutes les constructions que le scanner colore. | ||
| 20 | + | ||
| 21 | +## Guides pratiques — des recettes pour une tâche | ||
| 22 | + | ||
| 23 | +- [Installer et compiler Turbo Golo](how-to/install.md) | ||
| 24 | +- [Installer GoloScript](how-to/install-goloscript.md) | ||
| 25 | +- [Lancer les tests](how-to/run-the-tests.md) | ||
| 26 | +- [Activer la complétion Golo](how-to/enable-completion.md) | ||
| 27 | +- [Écrire son propre thème](how-to/write-a-theme.md) | ||
| 28 | +- [Se déplacer dans un fichier](how-to/navigate-code.md) | ||
| 29 | +- [Interroger le code](how-to/ask-about-code.md) | ||
| 30 | +- [Lancer des commandes shell sans quitter l'éditeur](how-to/use-a-terminal.md) | ||
| 31 | +- [Donner ses propres réglages à un projet](how-to/configure-a-project.md) | ||
| 32 | +- [Parcourir un projet et ouvrir des fichiers depuis un arbre](how-to/browse-a-project.md) | ||
| 33 | +- [Insérer des snippets depuis un menu](how-to/use-snippets.md) | ||
| 34 | +- [Lancer les commandes golo depuis l'éditeur](how-to/run-golo-commands.md) | ||
| 35 | +- [Faire une release](how-to/make-a-release.md) | ||
| 36 | +- [Dialoguer avec un agent de code depuis l'éditeur](how-to/talk-to-an-agent.md) | ||
| 37 | + | ||
| 38 | +## Référence — les détails exacts | ||
| 39 | + | ||
| 40 | +- [Ligne de commande](reference/cli.md) | ||
| 41 | +- [Clavier](reference/keyboard.md) | ||
| 42 | +- [Menus](reference/menus.md) | ||
| 43 | +- [Format des fichiers de thème](reference/themes.md) | ||
| 44 | +- [Fenêtres terminal](reference/terminal.md) | ||
| 45 | +- [Réglages de projet](reference/project-settings.md) | ||
| 46 | +- [Arbre du projet](reference/project-tree.md) | ||
| 47 | +- [Langages colorés](reference/languages.md) | ||
| 48 | +- [Snippets](reference/snippets.md) | ||
| 49 | +- [Outils Golo](reference/golo-tools.md) | ||
| 50 | +- [Le numéro de version](reference/versioning.md) | ||
| 51 | +- [Agents et ACP](reference/acp.md) | ||
| 52 | + | ||
| 53 | +## Explications — comprendre | ||
| 54 | + | ||
| 55 | +- [Architecture](explanation/architecture.md) | ||
| 56 | +- [Décisions de conception](explanation/design-decisions.md) | ||
| 57 | +- [Coloration et complétion](explanation/colouring-and-completion.md) | ||
| 58 | +- [Fenêtres terminal](explanation/terminal-windows.md) | ||
| 59 | +- [Réglages de projet](explanation/project-settings.md) | ||
| 60 | +- [Arbre du projet](explanation/project-tree.md) | ||
| 61 | +- [Snippets](explanation/snippets.md) | ||
| 62 | +- [Outils Golo](explanation/golo-tools.md) | ||
| 63 | +- [Fenêtres agent](explanation/agent-windows.md) | ||
added
docs/fr/explanation/agent-windows.md +114 -0 | new file mode 100644 | ||
| @@ -0,0 +1,114 @@ | ||
| 1 | +# Fenêtres agent | |
| 2 | + | |
| 3 | +Cette page explique pourquoi dialoguer avec un agent prend cette forme-là. Pour savoir comment faire, voir [Dialoguer avec un agent de code](../how-to/talk-to-an-agent.md) ; pour les touches et le format de fichier exacts, [Agents et ACP](../reference/acp.md). | |
| 4 | + | |
| 5 | +## Pourquoi un protocole plutôt qu'un fournisseur | |
| 6 | + | |
| 7 | +Un éditeur qui voulait offrir une fenêtre de conversation avait deux façons de l'obtenir. Parler directement aux fournisseurs de modèles — un client HTTP par fournisseur, un jeu de clés d'API à stocker, une boucle d'appel d'outils à écrire, et un nouvel exemplaire de chaque dès que quelqu'un veut un fournisseur dont l'éditeur n'a jamais entendu parler. Ou parler un seul protocole au programme auquel l'utilisateur fait déjà confiance pour ce travail. | |
| 8 | + | |
| 9 | +L'[Agent Client Protocol](https://agentclientprotocol.com) est la seconde. L'agent est un processus fils ; l'éditeur lui envoie des invites et dessine ce qui revient. L'éditeur ne détient aucune clé d'API, ne connaît aucun fournisseur, et n'implémente aucune boucle d'appel d'outils — et le même code parle à `docker agent` devant un llama.cpp local, à un agent dans le nuage, ou à quelque chose que vous avez écrit cet après-midi. | |
| 10 | + | |
| 11 | +Cela veut aussi dire que l'éditeur n'est pas l'endroit où atterrit un nouveau modèle. Sa prise en charge est une ligne dans le fichier de configuration de *votre* agent, un fichier que cet éditeur ne lit pas. | |
| 12 | + | |
| 13 | +## Pourquoi cela vit dans turbo-core | |
| 14 | + | |
| 15 | +Turbo Golo est [une commande, un profil et un analyseur](architecture.md) ; tout le reste est la bibliothèque que partagent tous les éditeurs Turbo. Une fenêtre agent est une fenêtre, un menu, une boîte modale et un tour de boucle d'événements — quatre choses qui appartiennent toutes à `turbo-core/app`. La construire ici aurait voulu dire ajouter à la bibliothèque une couture générale « laisser un éditeur ajouter une fenêtre et un menu depuis l'extérieur », puis s'en servir exactement une fois. | |
| 16 | + | |
| 17 | +Le client du protocole, le modèle de conversation et la fenêtre sont donc `turbo-core/acp`, à côté de `terminal` et `filetree`, qui ont la même forme. Ce que Turbo Golo apporte, c'est l'`acp.toml` de départ qu'il propose d'écrire — la seule part de tout ceci qui parle de projets Golo. Turbo Rust et Turbo Python obtiendront des fenêtres agent en écrivant un fichier de départ à eux, et rien d'autre. | |
| 18 | + | |
| 19 | +## Pourquoi une fenêtre, et non un panneau | |
| 20 | + | |
| 21 | +Le même raisonnement que celui tranché pour l'[arbre de projet](project-tree.md). Un panneau ancré voudrait dire que le bureau acquiert la notion de bords réservés, et que `fitInto`, les modes d'agrandissement, la maximisation, la mosaïque et la cascade doivent tous les respecter — une modification des fondations de l'interface pour un seul widget. En tant que fenêtre ordinaire, un agent obtient `F6`, les `Alt`-chiffres, `[x]`, `[■]` et Tile gratuitement. | |
| 22 | + | |
| 23 | +Cela fait aussi tomber « plusieurs agents à la fois » au lieu de le concevoir : deux fenêtres sont deux processus et deux conversations, et Tile met un modèle local rapide à côté d'un modèle lent et soigneux. Un panneau aurait dû se doter d'onglets pour en faire autant. | |
| 24 | + | |
| 25 | +## Pourquoi un processus par fenêtre, démarré à l'ouverture | |
| 26 | + | |
| 27 | +Un agent est une conversation, et une conversation a un début. Démarrer le processus avec la fenêtre fait que le dossier de travail de l'agent, son environnement et sa session appartiennent tous à cette fenêtre, et que la fermer est une fin sans ambiguïté — le même marché que passent les [fenêtres terminal](terminal-windows.md), et pour la même raison : ce que la fenêtre contient est un processus en cours, pas un travail non enregistré, donc la fermer ne demande rien. | |
| 28 | + | |
| 29 | +L'autre solution — un agent unique et durable multiplexé sur plusieurs fenêtres — aurait voulu dire que l'éditeur décide à quelle fenêtre appartient un `session/update`, et quoi faire d'une fenêtre dont la session a disparu alors que le processus vit encore. Deux processus coûtent moins cher que cette comptabilité. | |
| 30 | + | |
| 31 | +## Pourquoi la boîte de permission est ouverte depuis la boucle d'événements, et non depuis le message | |
| 32 | + | |
| 33 | +`session/request_permission` arrive sur la goroutine de lecture de la connexion, et la réponse vient d'une boîte de dialogue que l'utilisateur doit regarder. La réponse ne peut donc pas être faite là où la requête est traitée, et la boîte ne peut pas non plus y être ouverte : tout ce qui dessine appartient à la goroutine principale. | |
| 34 | + | |
| 35 | +La requête est donc *enregistrée*, et la boucle d'événements la remarque à son tour suivant et ouvre la boîte. C'est la quatrième fois que ce projet arrive à la même conclusion — l'[enregistrement automatique](project-settings.md), la ré-annonce au serveur de langage et les redessins du terminal sont les autres — et la raison est toujours la même : `PostEvent` a le droit de jeter ce qui ne rentre pas, donc un événement peut provoquer un tour de boucle mais ne doit jamais être le seul porteur d'un fait. | |
| 36 | + | |
| 37 | +C'est pourquoi la couche JSON-RPC a dû apprendre à répondre à une requête *plus tard*. C'est aussi toute la raison pour laquelle `jsonrpc` a été extrait de `lsp` : les questions d'un serveur de langage peuvent toutes être répondues sur-le-champ, et celles d'un agent non. | |
| 38 | + | |
| 39 | +## Pourquoi l'agent reçoit le tampon plutôt que le fichier | |
| 40 | + | |
| 41 | +Quand l'agent lit un fichier que vous avez ouvert et pas enregistré, il reçoit le texte que vous avez sous les yeux, pas celui du disque. L'autre solution est un agent qui relit la version que vous venez de dépasser, ce qui est faux précisément au moment où vous avez le plus de chances de poser la question — vous avez changé quelque chose et vous voulez savoir ce qu'il en est. | |
| 42 | + | |
| 43 | +Le coût est que l'agent voit un texte qu'aucun autre outil ne voit, donc une réponse citant un numéro de ligne peut ne pas correspondre à ce que dit `gogolo build`. C'est accepté : c'est déjà vrai de la complétion, qui répond depuis le tampon depuis que l'éditeur sait parler à `golo`. | |
| 44 | + | |
| 45 | +Les écritures suivent le même chemin, dans le tampon, marqué modifié. Un agent qui modifie un fichier laisse la modification sous vos yeux, annulable avec `Ctrl-Z` et non enregistrée jusqu'à ce que vous fassiez `F2`. Un agent réécrivant discrètement un fichier sous une fenêtre que vous avez ouverte serait la pire version possible de cette fonctionnalité. | |
| 46 | + | |
| 47 | +## Pourquoi les couleurs sont les classes syntaxiques, et non de nouvelles clés de thème | |
| 48 | + | |
| 49 | +L'[arbre de projet](project-tree.md) a eu besoin de clés de thème à lui, parce qu'il aurait sinon emprunté `list.selected`, une couleur choisie sur un fond de *dialogue*, et dessiné sa ligne sélectionnée dans la couleur qui se trouve dessous. Rien de tel ici : le corps d'une fenêtre agent est `window.body`, ce sur quoi les classes syntaxiques sont déjà choisies et déjà testées pour le contraste. | |
| 50 | + | |
| 51 | +Le nom d'un interlocuteur est donc dessiné dans le style des mots-clés, une réflexion dans celui des commentaires, un appel d'outil dans celui des types, et le code selon ce qu'en dit son propre analyseur. Onze thèmes colorent correctement les fenêtres agent sans avoir été touchés, et un thème écrit par quelqu'un l'an dernier aussi. | |
| 52 | + | |
| 53 | +Ce à quoi on renonce, c'est l'expressivité : un thème ne peut pas rendre les réflexions discrètes sans rendre aussi les commentaires discrets, puisque c'est la même clé. Si cela s'avère gênant à l'usage, des clés `agent.*` pourront être ajoutées plus tard — les règles de contraste et le test de complétude en sont le prix, et il ne vaut d'être payé que si quelqu'un veut la distinction. | |
| 54 | + | |
| 55 | +## Pourquoi la transcription est un modèle que la fenêtre se contente de dessiner | |
| 56 | + | |
| 57 | +L'agent envoie des jetons : `"I"`, `" found"`, `" agent"`, `".yaml"`. Une fenêtre qui ajouterait chacun à une liste de lignes serait une fenêtre incapable de reformater, incapable de distinguer la prose d'un bloc de code délimité, et impossible à tester sans agent vivant. | |
| 58 | + | |
| 59 | +La conversation est donc une valeur — `acp.Transcript` — qui fusionne les fragments en entrées, replie chaque `tool_call_update` sur le `tool_call` dont l'identifiant correspond, et remet à la fenêtre une liste de blocs qui sont soit de la prose, soit du code dans un langage nommé. Elle ne sait rien d'un terminal, ce qui permet de la tester en appelant des fonctions et en comparant des valeurs, la règle d'organisation que suivent déjà `buffer`, `lsp` et `syntax`. | |
| 60 | + | |
| 61 | +C'est aussi ce qui rend les tests de dessin déterministes. Le projet s'est déjà fait mordre par des tests qui portaient sur un écran pendant qu'un processus vivant y écrivait, et cela a caché un vrai défaut pendant toute une session ; une fenêtre dessinée à partir d'une transcription figée ne peut courir contre rien. | |
| 62 | + | |
| 63 | +## Ce qui a été volontairement laissé de côté | |
| 64 | + | |
| 65 | +- **La reprise de session.** `session/load` existe, et s'en servir voudrait dire décider où les conversations sont stockées, combien de temps elles sont gardées, et ce qui se passe quand le projet a déménagé. C'est une fonctionnalité à part entière. | |
| 66 | +- **L'authentification.** Un agent qui a besoin d'une connexion est prié de se connecter avec sa propre CLI. Stocker un identifiant est une responsabilité que cet éditeur a jusqu'ici entièrement évitée, et une méthode de protocole n'est pas une bonne raison de commencer. | |
| 67 | +- **La capacité `terminal`.** Un agent peut déjà avoir un shell par ses propres jeux d'outils, comme le fait `docker agent`. L'annoncer voudrait dire que l'éditeur lance des commandes pour le compte de l'agent et possède la sortie — le menu des outils fait déjà cela, et mieux, pour des commandes que *vous* avez choisies. | |
| 68 | +- **Les images dans les invites.** L'éditeur a du texte et des fichiers à envoyer, et un terminal pour dessiner. | |
| 69 | + | |
| 70 | +## Voir aussi | |
| 71 | + | |
| 72 | +- [Architecture](architecture.md) — ce qui est ici et ce qui est dans la bibliothèque | |
| 73 | +- [Fenêtres terminal](terminal-windows.md) — l'autre fenêtre qui contient un processus vivant | |
| 74 | +- [Arbre de projet](project-tree.md) — là où l'argument fenêtre-et-non-panneau a été posé la première fois | |
| 75 | + | |
| 76 | +## Pourquoi la copie va dans deux presse-papiers | |
| 77 | + | |
| 78 | +« Copie ça pour que je m'en serve ailleurs » veut généralement dire *complètement ailleurs* — une autre fenêtre, un navigateur, un message à un collègue. Un presse-papiers qui ne fonctionnerait qu'à l'intérieur de cet éditeur répondrait à la plus petite moitié de la demande, et à celle qu'on avait le moins de chances de poser. | |
| 79 | + | |
| 80 | +Une copie part donc dans les deux : celui de l'éditeur, d'où `Shift-Ins` colle, et celui du système, atteint en le demandant au terminal par OSC 52. Rien ne vérifie le second, parce qu'il n'y a rien à vérifier — la séquence n'a pas de réponse, un terminal peut la refuser par sécurité, et certains demandent qu'on l'active. Un message promettant quelque chose qui n'a pas eu lieu serait pire qu'un message qui se tait : la barre d'état dit seulement combien de lignes ont été copiées, ce qui est vrai dans les deux cas. | |
| 81 | + | |
| 82 | +## Pourquoi copier sans rien sélectionner copie un bloc entier | |
| 83 | + | |
| 84 | +Ce qu'on veut extraire d'une conversation, c'est presque toujours un bloc de code. Obliger à le sélectionner d'abord — six frappes, ou un glissement qu'il faut viser — est un travail que l'éditeur a déjà de quoi faire à votre place : c'est lui qui a mis la conversation en page, donc il sait exactement où ce bloc commence et finit. | |
| 85 | + | |
| 86 | +Les lignes portent donc une **région** : un bloc de code délimité, un passage de prose, la sortie d'un appel d'outil. Sans rien de sélectionné, `Ctrl-C` copie la région sur laquelle est le curseur. Le libellé d'un interlocuteur et l'en-tête d'un appel d'outil sont du mobilier et reçoivent des régions à eux, ce qui garde `‣ Bob (llama.cpp)` hors d'un bloc collé dans un fichier source. | |
| 87 | + | |
| 88 | +Cette dernière partie n'a pas été conçue, elle a été trouvée. La première version copiait le libellé avec le code ; c'est en copiant depuis le vrai binaire et en relisant la charge utile OSC 52 sur le fil qu'on s'en est aperçu. | |
| 89 | + | |
| 90 | +## Pourquoi l'indicateur d'activité est dessiné à partir de l'horloge | |
| 91 | + | |
| 92 | +Un agent qui réfléchit vingt secondes n'envoie strictement rien, et une fenêtre qui aurait l'air figée serait indiscernable d'une fenêtre réellement bloquée. L'indicateur est la réponse la moins chère possible à « est-ce que ça marche encore ? ». | |
| 93 | + | |
| 94 | +C'est une fonction du temps — `Spinner(now)` — et non un compteur que quelque chose incrémente. Rien n'a besoin d'être remis à zéro au début d'un tour, deux fenêtres qui réfléchissent en même temps tournent en phase, et un test peut porter sur une image sans attendre qu'elle arrive — la même raison pour laquelle `editor.View` et `app.App` ont tous deux une horloge injectable. | |
| 95 | + | |
| 96 | +Dessiner à partir de l'horloge veut dire qu'autre chose doit *provoquer* le redessin : une session en cours de tour réveille donc la boucle d'événements à la cadence de l'indicateur. Cela a le droit d'être un minuteur précisément parce qu'un battement perdu ne peut rien laisser en plan : il demande un tour de boucle et ne porte jamais de fait — la règle à laquelle ce projet est maintenant arrivé cinq fois. | |
| 97 | + | |
| 98 | +Le **titre** de la fenêtre, lui, n'est délibérément pas animé. C'est aussi ce qu'affichent la liste des fenêtres et le menu `Alt`-chiffre, et un nom qui changerait huit fois par seconde les ferait scintiller sans rien apporter. | |
| 99 | + | |
| 100 | +## Pourquoi les commandes sont une liste dans la zone de saisie, et non un menu | |
| 101 | + | |
| 102 | +Les commandes d'un agent arrivent sur le fil sous forme de liste — `available_commands_update` — et peuvent changer pendant la session. Un menu construit à partir d'elles devrait être reconstruit à chaque mise à jour, se trouverait loin de l'endroit où la commande se tape, et finirait quand même par mettre `/web ` dans la zone de saisie, parce que c'est la seule chose que le protocole laisse un client envoyer : une commande est une invite texte que l'agent reconnaît à son premier mot. | |
| 103 | + | |
| 104 | +La liste s'ouvre donc là où est le texte, sur le caractère qui commence une commande, et se ferme quand le mot est complet. C'est la même forme que la fenêtre de complétion au-dessus d'un fichier, pour la même raison : ce que vous choisissez est ce que vous tapez. Utiliser `/` et `@` plutôt que des touches propres à l'éditeur est délibéré — ce sont les caractères de Zed, si bien que la documentation d'un agent est vraie ici sans table de correspondance. | |
| 105 | + | |
| 106 | +`Entrée` a deux sens sur la liste, ordonnés par le degré d'achèvement du mot : elle complète un mot inachevé, et envoie un mot achevé. L'alternative — `Entrée` complète toujours, une seconde `Entrée` envoie — coûte une frappe à chaque commande et n'apporte rien, parce qu'un mot qui se lit déjà exactement comme une commande n'a plus rien à compléter. | |
| 107 | + | |
| 108 | +## Pourquoi une mention emporte le fichier, quand elle le peut | |
| 109 | + | |
| 110 | +Le protocole offre deux façons de nommer un fichier dans une invite : un `resource_link`, qui est une URI que l'agent va chercher lui-même, et une `resource` incorporée, qui est l'URI *et le texte*. La spécification appelle la seconde « la façon préférée d'inclure du contexte », et la raison est celle qui fait répondre `fs/read_text_file` depuis le tampon : l'éditeur sait sur le fichier des choses que le disque ignore. Un agent qui suit un lien vers un fichier que vous avez modifié sans l'enregistrer lit la version que vous venez de quitter, ce qui est faux précisément au moment où vous êtes le plus susceptible de demander. | |
| 111 | + | |
| 112 | +L'éditeur envoie donc le texte quand l'agent a déclaré `promptCapabilities.embeddedContext`, lu par le même chemin que `fs/read_text_file`, et un lien sinon — jamais rien. Un fichier qui ne peut pas être lu part aussi en lien, pour que l'agent sache au moins quel fichier était visé. | |
| 113 | + | |
| 114 | +La mention remplace le nom dans le texte plutôt que de voyager à côté. Envoyer `explique @main.go` comme texte *et* comme pièce jointe donnerait à l'agent le nom deux fois en le laissant les apparier ; mettre le bloc là où était le nom lui donne le fichier là où la phrase en a besoin. La conversation, elle, garde la ligne telle que tapée : c'est ce que vous avez dit, et la fenêtre est le compte rendu de la conversation, pas du fil. | |
| new file mode 100644 | |||
| @@ -0,0 +1,114 @@ | |||
| 1 | +# Fenêtres agent | ||
| 2 | + | ||
| 3 | +Cette page explique pourquoi dialoguer avec un agent prend cette forme-là. Pour savoir comment faire, voir [Dialoguer avec un agent de code](../how-to/talk-to-an-agent.md) ; pour les touches et le format de fichier exacts, [Agents et ACP](../reference/acp.md). | ||
| 4 | + | ||
| 5 | +## Pourquoi un protocole plutôt qu'un fournisseur | ||
| 6 | + | ||
| 7 | +Un éditeur qui voulait offrir une fenêtre de conversation avait deux façons de l'obtenir. Parler directement aux fournisseurs de modèles — un client HTTP par fournisseur, un jeu de clés d'API à stocker, une boucle d'appel d'outils à écrire, et un nouvel exemplaire de chaque dès que quelqu'un veut un fournisseur dont l'éditeur n'a jamais entendu parler. Ou parler un seul protocole au programme auquel l'utilisateur fait déjà confiance pour ce travail. | ||
| 8 | + | ||
| 9 | +L'[Agent Client Protocol](https://agentclientprotocol.com) est la seconde. L'agent est un processus fils ; l'éditeur lui envoie des invites et dessine ce qui revient. L'éditeur ne détient aucune clé d'API, ne connaît aucun fournisseur, et n'implémente aucune boucle d'appel d'outils — et le même code parle à `docker agent` devant un llama.cpp local, à un agent dans le nuage, ou à quelque chose que vous avez écrit cet après-midi. | ||
| 10 | + | ||
| 11 | +Cela veut aussi dire que l'éditeur n'est pas l'endroit où atterrit un nouveau modèle. Sa prise en charge est une ligne dans le fichier de configuration de *votre* agent, un fichier que cet éditeur ne lit pas. | ||
| 12 | + | ||
| 13 | +## Pourquoi cela vit dans turbo-core | ||
| 14 | + | ||
| 15 | +Turbo Golo est [une commande, un profil et un analyseur](architecture.md) ; tout le reste est la bibliothèque que partagent tous les éditeurs Turbo. Une fenêtre agent est une fenêtre, un menu, une boîte modale et un tour de boucle d'événements — quatre choses qui appartiennent toutes à `turbo-core/app`. La construire ici aurait voulu dire ajouter à la bibliothèque une couture générale « laisser un éditeur ajouter une fenêtre et un menu depuis l'extérieur », puis s'en servir exactement une fois. | ||
| 16 | + | ||
| 17 | +Le client du protocole, le modèle de conversation et la fenêtre sont donc `turbo-core/acp`, à côté de `terminal` et `filetree`, qui ont la même forme. Ce que Turbo Golo apporte, c'est l'`acp.toml` de départ qu'il propose d'écrire — la seule part de tout ceci qui parle de projets Golo. Turbo Rust et Turbo Python obtiendront des fenêtres agent en écrivant un fichier de départ à eux, et rien d'autre. | ||
| 18 | + | ||
| 19 | +## Pourquoi une fenêtre, et non un panneau | ||
| 20 | + | ||
| 21 | +Le même raisonnement que celui tranché pour l'[arbre de projet](project-tree.md). Un panneau ancré voudrait dire que le bureau acquiert la notion de bords réservés, et que `fitInto`, les modes d'agrandissement, la maximisation, la mosaïque et la cascade doivent tous les respecter — une modification des fondations de l'interface pour un seul widget. En tant que fenêtre ordinaire, un agent obtient `F6`, les `Alt`-chiffres, `[x]`, `[■]` et Tile gratuitement. | ||
| 22 | + | ||
| 23 | +Cela fait aussi tomber « plusieurs agents à la fois » au lieu de le concevoir : deux fenêtres sont deux processus et deux conversations, et Tile met un modèle local rapide à côté d'un modèle lent et soigneux. Un panneau aurait dû se doter d'onglets pour en faire autant. | ||
| 24 | + | ||
| 25 | +## Pourquoi un processus par fenêtre, démarré à l'ouverture | ||
| 26 | + | ||
| 27 | +Un agent est une conversation, et une conversation a un début. Démarrer le processus avec la fenêtre fait que le dossier de travail de l'agent, son environnement et sa session appartiennent tous à cette fenêtre, et que la fermer est une fin sans ambiguïté — le même marché que passent les [fenêtres terminal](terminal-windows.md), et pour la même raison : ce que la fenêtre contient est un processus en cours, pas un travail non enregistré, donc la fermer ne demande rien. | ||
| 28 | + | ||
| 29 | +L'autre solution — un agent unique et durable multiplexé sur plusieurs fenêtres — aurait voulu dire que l'éditeur décide à quelle fenêtre appartient un `session/update`, et quoi faire d'une fenêtre dont la session a disparu alors que le processus vit encore. Deux processus coûtent moins cher que cette comptabilité. | ||
| 30 | + | ||
| 31 | +## Pourquoi la boîte de permission est ouverte depuis la boucle d'événements, et non depuis le message | ||
| 32 | + | ||
| 33 | +`session/request_permission` arrive sur la goroutine de lecture de la connexion, et la réponse vient d'une boîte de dialogue que l'utilisateur doit regarder. La réponse ne peut donc pas être faite là où la requête est traitée, et la boîte ne peut pas non plus y être ouverte : tout ce qui dessine appartient à la goroutine principale. | ||
| 34 | + | ||
| 35 | +La requête est donc *enregistrée*, et la boucle d'événements la remarque à son tour suivant et ouvre la boîte. C'est la quatrième fois que ce projet arrive à la même conclusion — l'[enregistrement automatique](project-settings.md), la ré-annonce au serveur de langage et les redessins du terminal sont les autres — et la raison est toujours la même : `PostEvent` a le droit de jeter ce qui ne rentre pas, donc un événement peut provoquer un tour de boucle mais ne doit jamais être le seul porteur d'un fait. | ||
| 36 | + | ||
| 37 | +C'est pourquoi la couche JSON-RPC a dû apprendre à répondre à une requête *plus tard*. C'est aussi toute la raison pour laquelle `jsonrpc` a été extrait de `lsp` : les questions d'un serveur de langage peuvent toutes être répondues sur-le-champ, et celles d'un agent non. | ||
| 38 | + | ||
| 39 | +## Pourquoi l'agent reçoit le tampon plutôt que le fichier | ||
| 40 | + | ||
| 41 | +Quand l'agent lit un fichier que vous avez ouvert et pas enregistré, il reçoit le texte que vous avez sous les yeux, pas celui du disque. L'autre solution est un agent qui relit la version que vous venez de dépasser, ce qui est faux précisément au moment où vous avez le plus de chances de poser la question — vous avez changé quelque chose et vous voulez savoir ce qu'il en est. | ||
| 42 | + | ||
| 43 | +Le coût est que l'agent voit un texte qu'aucun autre outil ne voit, donc une réponse citant un numéro de ligne peut ne pas correspondre à ce que dit `gogolo build`. C'est accepté : c'est déjà vrai de la complétion, qui répond depuis le tampon depuis que l'éditeur sait parler à `golo`. | ||
| 44 | + | ||
| 45 | +Les écritures suivent le même chemin, dans le tampon, marqué modifié. Un agent qui modifie un fichier laisse la modification sous vos yeux, annulable avec `Ctrl-Z` et non enregistrée jusqu'à ce que vous fassiez `F2`. Un agent réécrivant discrètement un fichier sous une fenêtre que vous avez ouverte serait la pire version possible de cette fonctionnalité. | ||
| 46 | + | ||
| 47 | +## Pourquoi les couleurs sont les classes syntaxiques, et non de nouvelles clés de thème | ||
| 48 | + | ||
| 49 | +L'[arbre de projet](project-tree.md) a eu besoin de clés de thème à lui, parce qu'il aurait sinon emprunté `list.selected`, une couleur choisie sur un fond de *dialogue*, et dessiné sa ligne sélectionnée dans la couleur qui se trouve dessous. Rien de tel ici : le corps d'une fenêtre agent est `window.body`, ce sur quoi les classes syntaxiques sont déjà choisies et déjà testées pour le contraste. | ||
| 50 | + | ||
| 51 | +Le nom d'un interlocuteur est donc dessiné dans le style des mots-clés, une réflexion dans celui des commentaires, un appel d'outil dans celui des types, et le code selon ce qu'en dit son propre analyseur. Onze thèmes colorent correctement les fenêtres agent sans avoir été touchés, et un thème écrit par quelqu'un l'an dernier aussi. | ||
| 52 | + | ||
| 53 | +Ce à quoi on renonce, c'est l'expressivité : un thème ne peut pas rendre les réflexions discrètes sans rendre aussi les commentaires discrets, puisque c'est la même clé. Si cela s'avère gênant à l'usage, des clés `agent.*` pourront être ajoutées plus tard — les règles de contraste et le test de complétude en sont le prix, et il ne vaut d'être payé que si quelqu'un veut la distinction. | ||
| 54 | + | ||
| 55 | +## Pourquoi la transcription est un modèle que la fenêtre se contente de dessiner | ||
| 56 | + | ||
| 57 | +L'agent envoie des jetons : `"I"`, `" found"`, `" agent"`, `".yaml"`. Une fenêtre qui ajouterait chacun à une liste de lignes serait une fenêtre incapable de reformater, incapable de distinguer la prose d'un bloc de code délimité, et impossible à tester sans agent vivant. | ||
| 58 | + | ||
| 59 | +La conversation est donc une valeur — `acp.Transcript` — qui fusionne les fragments en entrées, replie chaque `tool_call_update` sur le `tool_call` dont l'identifiant correspond, et remet à la fenêtre une liste de blocs qui sont soit de la prose, soit du code dans un langage nommé. Elle ne sait rien d'un terminal, ce qui permet de la tester en appelant des fonctions et en comparant des valeurs, la règle d'organisation que suivent déjà `buffer`, `lsp` et `syntax`. | ||
| 60 | + | ||
| 61 | +C'est aussi ce qui rend les tests de dessin déterministes. Le projet s'est déjà fait mordre par des tests qui portaient sur un écran pendant qu'un processus vivant y écrivait, et cela a caché un vrai défaut pendant toute une session ; une fenêtre dessinée à partir d'une transcription figée ne peut courir contre rien. | ||
| 62 | + | ||
| 63 | +## Ce qui a été volontairement laissé de côté | ||
| 64 | + | ||
| 65 | +- **La reprise de session.** `session/load` existe, et s'en servir voudrait dire décider où les conversations sont stockées, combien de temps elles sont gardées, et ce qui se passe quand le projet a déménagé. C'est une fonctionnalité à part entière. | ||
| 66 | +- **L'authentification.** Un agent qui a besoin d'une connexion est prié de se connecter avec sa propre CLI. Stocker un identifiant est une responsabilité que cet éditeur a jusqu'ici entièrement évitée, et une méthode de protocole n'est pas une bonne raison de commencer. | ||
| 67 | +- **La capacité `terminal`.** Un agent peut déjà avoir un shell par ses propres jeux d'outils, comme le fait `docker agent`. L'annoncer voudrait dire que l'éditeur lance des commandes pour le compte de l'agent et possède la sortie — le menu des outils fait déjà cela, et mieux, pour des commandes que *vous* avez choisies. | ||
| 68 | +- **Les images dans les invites.** L'éditeur a du texte et des fichiers à envoyer, et un terminal pour dessiner. | ||
| 69 | + | ||
| 70 | +## Voir aussi | ||
| 71 | + | ||
| 72 | +- [Architecture](architecture.md) — ce qui est ici et ce qui est dans la bibliothèque | ||
| 73 | +- [Fenêtres terminal](terminal-windows.md) — l'autre fenêtre qui contient un processus vivant | ||
| 74 | +- [Arbre de projet](project-tree.md) — là où l'argument fenêtre-et-non-panneau a été posé la première fois | ||
| 75 | + | ||
| 76 | +## Pourquoi la copie va dans deux presse-papiers | ||
| 77 | + | ||
| 78 | +« Copie ça pour que je m'en serve ailleurs » veut généralement dire *complètement ailleurs* — une autre fenêtre, un navigateur, un message à un collègue. Un presse-papiers qui ne fonctionnerait qu'à l'intérieur de cet éditeur répondrait à la plus petite moitié de la demande, et à celle qu'on avait le moins de chances de poser. | ||
| 79 | + | ||
| 80 | +Une copie part donc dans les deux : celui de l'éditeur, d'où `Shift-Ins` colle, et celui du système, atteint en le demandant au terminal par OSC 52. Rien ne vérifie le second, parce qu'il n'y a rien à vérifier — la séquence n'a pas de réponse, un terminal peut la refuser par sécurité, et certains demandent qu'on l'active. Un message promettant quelque chose qui n'a pas eu lieu serait pire qu'un message qui se tait : la barre d'état dit seulement combien de lignes ont été copiées, ce qui est vrai dans les deux cas. | ||
| 81 | + | ||
| 82 | +## Pourquoi copier sans rien sélectionner copie un bloc entier | ||
| 83 | + | ||
| 84 | +Ce qu'on veut extraire d'une conversation, c'est presque toujours un bloc de code. Obliger à le sélectionner d'abord — six frappes, ou un glissement qu'il faut viser — est un travail que l'éditeur a déjà de quoi faire à votre place : c'est lui qui a mis la conversation en page, donc il sait exactement où ce bloc commence et finit. | ||
| 85 | + | ||
| 86 | +Les lignes portent donc une **région** : un bloc de code délimité, un passage de prose, la sortie d'un appel d'outil. Sans rien de sélectionné, `Ctrl-C` copie la région sur laquelle est le curseur. Le libellé d'un interlocuteur et l'en-tête d'un appel d'outil sont du mobilier et reçoivent des régions à eux, ce qui garde `‣ Bob (llama.cpp)` hors d'un bloc collé dans un fichier source. | ||
| 87 | + | ||
| 88 | +Cette dernière partie n'a pas été conçue, elle a été trouvée. La première version copiait le libellé avec le code ; c'est en copiant depuis le vrai binaire et en relisant la charge utile OSC 52 sur le fil qu'on s'en est aperçu. | ||
| 89 | + | ||
| 90 | +## Pourquoi l'indicateur d'activité est dessiné à partir de l'horloge | ||
| 91 | + | ||
| 92 | +Un agent qui réfléchit vingt secondes n'envoie strictement rien, et une fenêtre qui aurait l'air figée serait indiscernable d'une fenêtre réellement bloquée. L'indicateur est la réponse la moins chère possible à « est-ce que ça marche encore ? ». | ||
| 93 | + | ||
| 94 | +C'est une fonction du temps — `Spinner(now)` — et non un compteur que quelque chose incrémente. Rien n'a besoin d'être remis à zéro au début d'un tour, deux fenêtres qui réfléchissent en même temps tournent en phase, et un test peut porter sur une image sans attendre qu'elle arrive — la même raison pour laquelle `editor.View` et `app.App` ont tous deux une horloge injectable. | ||
| 95 | + | ||
| 96 | +Dessiner à partir de l'horloge veut dire qu'autre chose doit *provoquer* le redessin : une session en cours de tour réveille donc la boucle d'événements à la cadence de l'indicateur. Cela a le droit d'être un minuteur précisément parce qu'un battement perdu ne peut rien laisser en plan : il demande un tour de boucle et ne porte jamais de fait — la règle à laquelle ce projet est maintenant arrivé cinq fois. | ||
| 97 | + | ||
| 98 | +Le **titre** de la fenêtre, lui, n'est délibérément pas animé. C'est aussi ce qu'affichent la liste des fenêtres et le menu `Alt`-chiffre, et un nom qui changerait huit fois par seconde les ferait scintiller sans rien apporter. | ||
| 99 | + | ||
| 100 | +## Pourquoi les commandes sont une liste dans la zone de saisie, et non un menu | ||
| 101 | + | ||
| 102 | +Les commandes d'un agent arrivent sur le fil sous forme de liste — `available_commands_update` — et peuvent changer pendant la session. Un menu construit à partir d'elles devrait être reconstruit à chaque mise à jour, se trouverait loin de l'endroit où la commande se tape, et finirait quand même par mettre `/web ` dans la zone de saisie, parce que c'est la seule chose que le protocole laisse un client envoyer : une commande est une invite texte que l'agent reconnaît à son premier mot. | ||
| 103 | + | ||
| 104 | +La liste s'ouvre donc là où est le texte, sur le caractère qui commence une commande, et se ferme quand le mot est complet. C'est la même forme que la fenêtre de complétion au-dessus d'un fichier, pour la même raison : ce que vous choisissez est ce que vous tapez. Utiliser `/` et `@` plutôt que des touches propres à l'éditeur est délibéré — ce sont les caractères de Zed, si bien que la documentation d'un agent est vraie ici sans table de correspondance. | ||
| 105 | + | ||
| 106 | +`Entrée` a deux sens sur la liste, ordonnés par le degré d'achèvement du mot : elle complète un mot inachevé, et envoie un mot achevé. L'alternative — `Entrée` complète toujours, une seconde `Entrée` envoie — coûte une frappe à chaque commande et n'apporte rien, parce qu'un mot qui se lit déjà exactement comme une commande n'a plus rien à compléter. | ||
| 107 | + | ||
| 108 | +## Pourquoi une mention emporte le fichier, quand elle le peut | ||
| 109 | + | ||
| 110 | +Le protocole offre deux façons de nommer un fichier dans une invite : un `resource_link`, qui est une URI que l'agent va chercher lui-même, et une `resource` incorporée, qui est l'URI *et le texte*. La spécification appelle la seconde « la façon préférée d'inclure du contexte », et la raison est celle qui fait répondre `fs/read_text_file` depuis le tampon : l'éditeur sait sur le fichier des choses que le disque ignore. Un agent qui suit un lien vers un fichier que vous avez modifié sans l'enregistrer lit la version que vous venez de quitter, ce qui est faux précisément au moment où vous êtes le plus susceptible de demander. | ||
| 111 | + | ||
| 112 | +L'éditeur envoie donc le texte quand l'agent a déclaré `promptCapabilities.embeddedContext`, lu par le même chemin que `fs/read_text_file`, et un lien sinon — jamais rien. Un fichier qui ne peut pas être lu part aussi en lien, pour que l'agent sache au moins quel fichier était visé. | ||
| 113 | + | ||
| 114 | +La mention remplace le nom dans le texte plutôt que de voyager à côté. Envoyer `explique @main.go` comme texte *et* comme pièce jointe donnerait à l'agent le nom deux fois en le laissant les apparier ; mettre le bloc là où était le nom lui donne le fichier là où la phrase en a besoin. La conversation, elle, garde la ligne telle que tapée : c'est ce que vous avez dit, et la fenêtre est le compte rendu de la conversation, pas du fil. | ||
added
docs/fr/explanation/architecture.md +111 -0 | new file mode 100644 | ||
| @@ -0,0 +1,111 @@ | ||
| 1 | +# Architecture — explication | |
| 2 | + | |
| 3 | +## De quoi s'agit-il ? | |
| 4 | + | |
| 5 | +Turbo Golo, c'est une commande, un profil et un scanner. Tout le reste — le widget d'édition, les fenêtres, les menus, les dialogues, les thèmes, l'émulateur de terminal, l'arbre de fichiers, le client LSP — c'est [turbo-core](https://rickub.com/turbo-editors/turbo-core), la bibliothèque sur laquelle tous les éditeurs Turbo sont construits. | |
| 6 | + | |
| 7 | +Cette page parle de cette séparation : ce qui est ici, ce qui est là-bas, et pourquoi la frontière passe où elle passe. | |
| 8 | + | |
| 9 | +## Ce que contient ce dépôt | |
| 10 | + | |
| 11 | +``` | |
| 12 | +main.go les options, le terminal et le câblage | |
| 13 | +internal/gololang tout ce qui fait de cet éditeur Turbo Golo | |
| 14 | + gololang.go le profil : nom, menu, serveur, où golo est installé | |
| 15 | + scan.go le répartiteur du scanner, les commentaires, ce qui franchit une ligne | |
| 16 | + literals.go les trois formes entre guillemets — "…", """…""" et '…' | |
| 17 | + words.go les nombres, les mots-clés, les 157 builtins, les conventions de nommage | |
| 18 | + templates.go trois déclarations //go:embed | |
| 19 | + *.toml.tmpl les trois fichiers de départ d'un projet, embarqués | |
| 20 | +``` | |
| 21 | + | |
| 22 | +Un millier de lignes environ en comptant les commentaires, dont quelque six cents pour le scanner — moins de cinq cents lignes de code au compte de qlty, et un tiers de celles-ci est la table des builtins. Il n'y a pas d'`internal/app`, pas d'`internal/ui`, pas d'`internal/buffer` : ceux-là existent une fois, dans la bibliothèque, et les six éditeurs les utilisent tels quels. | |
| 23 | + | |
| 24 | +## Ce que fait `main` | |
| 25 | + | |
| 26 | +Six choses, dans cet ordre : | |
| 27 | + | |
| 28 | +1. Il lit les options. | |
| 29 | +2. Il appelle `gololang.Register()`, qui apprend à la bibliothèque à colorer les fichiers `.golo` et les scripts dont la première ligne nomme `golo`. | |
| 30 | +3. Il construit `gololang.Profile()` — la valeur qui dit que cet éditeur est Turbo Golo. | |
| 31 | +4. Il lit `.turbo-golo/settings.toml` dans le répertoire courant, s'il existe. | |
| 32 | +5. Il ouvre le terminal et confie l'écran, le nom du thème et le profil à `app.New`. | |
| 33 | +6. Il démarre `golo lsp` dans le répertoire du fichier édité, et lance la boucle d'événements. | |
| 34 | + | |
| 35 | +C'est toute la commande. Chaque décision qu'elle prend — quel thème l'emporte, quels fichiers ouvrir, faut-il démarrer un serveur de langage — porte sur *cette exécution*, pas sur Golo. | |
| 36 | + | |
| 37 | +## Le profil est la couture | |
| 38 | + | |
| 39 | +```go | |
| 40 | +profile.Profile{ | |
| 41 | + Name: "Turbo Golo", | |
| 42 | + Slug: "turbo-golo", | |
| 43 | + Language: "Golo", | |
| 44 | + ToolsMenu: "~G~olo", | |
| 45 | + RootMarkers: nil, | |
| 46 | + Server: profile.Server{Command: "golo", Args: []string{"lsp"}, …}, | |
| 47 | + Templates: profile.Templates{Settings: …, Snippets: …, Tools: …}, | |
| 48 | +} | |
| 49 | +``` | |
| 50 | + | |
| 51 | +Tout ce qui serait sinon un `"turbo-golo"`, un `"golo"` ou un `".golo"` codé en dur quelque part dans onze mille lignes est un champ ici. La bibliothèque les lit ; rien dans la bibliothèque ne sait ce qu'ils signifient. | |
| 52 | + | |
| 53 | +`Slug` porte plus qu'il n'y paraît. Le binaire s'appelle `turbo-golo`, le répertoire de projet `.turbo-golo`, la configuration personnelle vit dans `~/.config/turbo-golo`, et les variables d'environnement qui la remplacent sont `TURBO_GOLO_THEME_DIR` et `TURBO_GOLO_SNIPPET_DIR` — toutes dérivées de ce seul mot. | |
| 54 | + | |
| 55 | +`RootMarkers` est le seul champ vide ici et rempli chez tous les frères. Go a `go.mod`, Rust `Cargo.toml`, Python `pyproject.toml`, MoonBit `moon.mod` ; Golo n'a aucun manifeste. Un script est un fichier et un programme est un répertoire de fichiers, il n'y a donc rien vers quoi remonter, et le `ProjectRoot` de la bibliothèque — sans marqueur — répond par le répertoire du fichier. C'est aussi tout ce dont `golo lsp` a besoin : il répond à propos du fichier qu'on lui donne et résout les imports depuis les modules embarqués dans le binaire, jamais depuis le disque. | |
| 56 | + | |
| 57 | +## Pourquoi le scanner est ici et pas dans la bibliothèque | |
| 58 | + | |
| 59 | +turbo-core colore huit langages lui-même : TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell. Ce sont ceux que tout éditeur rencontre quel que soit son langage — la configuration d'un projet est en TOML ou en YAML, sa documentation en Markdown, ses scripts en shell, la construction de son image dans un Dockerfile. | |
| 60 | + | |
| 61 | +Golo n'en fait pas partie, ni Go, ni Rust, ni Python, ni MoonBit. Le langage qui *définit* un éditeur est enregistré par cet éditeur, et c'est pourquoi un fichier `.mbt` s'ouvre en texte brut ici et un fichier `.golo` s'ouvre en texte brut dans Turbo MoonBit. | |
| 62 | + | |
| 63 | +Cela aurait pu aller dans l'autre sens. Mettre les six scanners dans la bibliothèque permettrait à n'importe quel éditeur de colorer n'importe lequel de ces langages, sans coût en dépendances — un scanner Golo est du Go ordinaire. L'idée a été rejetée parce que la bibliothèque grossirait d'un langage chaque fois que quelqu'un construit un éditeur, et parce que « qu'est-ce que cet éditeur enregistre ? » cesserait d'être la première question à poser sur un nouveau venu. | |
| 64 | + | |
| 65 | +## Pourquoi le scanner n'a pas été emprunté à GoloScript | |
| 66 | + | |
| 67 | +GoloScript est écrit en Go, et son paquet `lexer` est exactement le tokeniseur que ce scanner réimplémente. Turbo Go se sert de `go/scanner` dans la même situation, la question est donc légitime. | |
| 68 | + | |
| 69 | +La réponse tient au nom du module. Le `go.mod` de GoloScript déclare `module golo`, un nom nu sans hôte, et le système de modules de Go ne sait pas aller chercher un module portant un tel chemin où que ce soit : importer `golo/lexer` depuis un autre module exige une directive `replace` pointant vers un dépôt cloné à côté, et un `replace` committé est précisément ce que `01-release.tag.sh` refuse de publier. Les règles du lexer sont donc reportées ici plutôt qu'appelées — et `lexer/lexer.go` et `token/token.go` sont la spécification contre laquelle le scanner est écrit, ligne pour ligne là où cela compte. La [page sur la coloration](colouring-and-completion.md) nomme les endroits où cette spécification et le parseur de l'interpréteur ne sont pas d'accord. | |
| 70 | + | |
| 71 | +## Pourquoi le menu de l'outillage s'appelle `~G~olo` et non `golo`, `gogolo` ou `wagolo` | |
| 72 | + | |
| 73 | +La touche chaude était la partie facile. Neuf lettres sont prises par les menus fixes — F, E, S, R, C, O, W, N et H — et `G` n'en fait pas partie, elle tombe donc sur la première lettre du mot, ce qui ne coûte à personne un second regard. Turbo Rust n'a pas eu cette chance et a fini sur `Rus~t~`. | |
| 74 | + | |
| 75 | +Le nom était la vraie décision, et elle a été prise comme chez tous les frères. Le menu contient ce que le projet a mis dans son fichier d'outils, et ce n'est pas toujours l'interpréteur : GoloScript lui-même, c'est trois binaires — `golo`, `gogolo`, `wagolo` — et le premier fichier d'outils qu'on écrit dépasse les trois, parce que les commandes d'un projet comprennent des conteneurs, des bases de données et une cible de `Makefile` ajoutée en 2019. Un menu appelé **golo** qui contient `wagolo build` est déjà un petit mensonge, et un qui contient `docker compose up` en est un gros. `Golo` est le langage, et le langage est ce pour quoi cet éditeur existe. | |
| 76 | + | |
| 77 | +## Pourquoi les tests pilotent le vrai éditeur | |
| 78 | + | |
| 79 | +`internal/gololang/editor_test.go` construit un Turbo Golo entier sur un terminal simulé — `app.New(screen, "turbo-classic", gololang.Profile())` — ouvre un fichier et vérifie la coloration, la barre de menus et les touches chaudes. Il n'utilise que l'API publique de la bibliothèque. | |
| 80 | + | |
| 81 | +C'est délibéré. La suite de la bibliothèque prouve que la bibliothèque fonctionne ; ce que ces tests prouvent, c'est que *cet éditeur est correctement assemblé* — que `Register` a été appelé, que le profil a atteint la barre de menus, qu'un fichier `.golo` sort coloré et qu'un script avec un shebang `golo` aussi. Un bogue où `main` oublierait d'enregistrer Golo passerait tous les tests de turbo-core. | |
| 82 | + | |
| 83 | +Le même fichier pilote un **vrai `golo lsp`** de bout en bout, dix fois. Il écrit un script, l'ouvre, démarre le serveur, puis : | |
| 84 | + | |
| 85 | +- **tape une déclaration de fonction qui n'existe que dans le tampon**, puis tape ses premières lettres sur une autre ligne et demande une complétion — `golo lsp` propose les mots-clés et les builtins pour n'importe quel fichier, une complétion contenant `println` ne prouverait donc rien ; une qui contient une fonction absente du disque prouve que le tampon a été envoyé ; | |
| 86 | +- demande la **complétion d'un préfixe vide** et compare la réponse aux tables du scanner dans les deux sens — chaque mot-clé et chaque builtin que le scanner colore doit être proposé par le serveur, et tout ce que le serveur propose doit être connu du scanner, ce qui tient la table de 157 builtins à l'interpréteur plutôt qu'à la mémoire ; | |
| 87 | +- demande la **définition** d'un appel et le **survol** de celui-ci, qui revient avec le commentaire `#` écrit au-dessus de la déclaration ; | |
| 88 | +- demande les **symboles du fichier** ; | |
| 89 | +- demande les **références** d'un appel — la déclaration et chaque appel dans le fichier — et son **implémentation**, qui est la déclaration elle-même, Golo n'ayant pas d'interfaces ; | |
| 90 | +- cherche un **symbole dans le projet**, y compris dans un fichier que l'éditeur n'a jamais ouvert ; | |
| 91 | +- ouvre un fichier qui **ne parse pas** et attend qu'un diagnostic arrive sans qu'on le demande — la seule fonctionnalité dont l'échec ressemble exactement au succès, parce qu'un éditeur qui n'a aucune erreur à montrer et un éditeur qui ne trouve pas l'erreur ont la même gouttière vide ; | |
| 92 | +- ouvre un fichier contenant un **commentaire `//` à la C** et attend le lint qui dit que Golo utilise `#`. | |
| 93 | + | |
| 94 | +Un dernier test épingle ce que `golo lsp` *ne sait toujours pas* faire : il n'annonce pas `typeDefinition`, la documentation le dit, et le test échoue si un futur golo se met à répondre — la page est alors revue plutôt que de vieillir en silence. C'est ainsi que les trois tests qui le précèdent sont nés : jusqu'à GoloScript v0.2.0 le même test épinglait les références, les implémentations et les symboles du projet comme refus, et il est passé au rouge le jour où le serveur les a appris. | |
| 95 | + | |
| 96 | +## Alternatives rejetées | |
| 97 | + | |
| 98 | +**Forker Turbo MoonBit.** La façon évidente d'obtenir un cinquième éditeur, et la raison pour laquelle la bibliothèque existe à la place : cinq copies de onze mille lignes divergent en un mois, et chaque correction doit être faite cinq fois par quelqu'un qui se souvient qu'il y en a cinq. | |
| 99 | + | |
| 100 | +**Un système de plugins.** Turbo Golo est un programme Go qui importe une bibliothèque. Il n'y a ni chargement dynamique ni ABI. En ajouter un reviendrait à figer l'API de chaque paquet de turbo-core plutôt que celle des quelques-uns qu'un profil touche. | |
| 101 | + | |
| 102 | +**Un fichier de configuration au lieu d'un profil.** Le profil aurait pu être du TOML lu au démarrage, ce qui ferait d'un nouvel éditeur un fichier plutôt qu'un programme. Cela rendrait aussi le scanner inexprimable, et un éditeur à moitié configurable — tout sauf la coloration — est pire que l'une ou l'autre des réponses entières. | |
| 103 | + | |
| 104 | +**Importer le lexer de GoloScript.** Pesé plus haut : le module ne peut pas être récupéré, et un `replace` ne peut pas être publié. | |
| 105 | + | |
| 106 | +## Liens avec le reste | |
| 107 | + | |
| 108 | +- Ce que fait chaque paquet de la bibliothèque : [la référence des paquets de turbo-core](https://rickub.com/turbo-editors/turbo-core/blob/main/docs/fr/reference/packages.md) | |
| 109 | +- Comment la coloration fonctionne ici : [Coloration et complétion](colouring-and-completion.md) | |
| 110 | +- Pourquoi le menu des outils est une donnée : [Outils Golo](golo-tools.md) | |
| 111 | +- Les décisions qui ont survécu au refactoring : [Décisions de conception](design-decisions.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,111 @@ | |||
| 1 | +# Architecture — explication | ||
| 2 | + | ||
| 3 | +## De quoi s'agit-il ? | ||
| 4 | + | ||
| 5 | +Turbo Golo, c'est une commande, un profil et un scanner. Tout le reste — le widget d'édition, les fenêtres, les menus, les dialogues, les thèmes, l'émulateur de terminal, l'arbre de fichiers, le client LSP — c'est [turbo-core](https://rickub.com/turbo-editors/turbo-core), la bibliothèque sur laquelle tous les éditeurs Turbo sont construits. | ||
| 6 | + | ||
| 7 | +Cette page parle de cette séparation : ce qui est ici, ce qui est là-bas, et pourquoi la frontière passe où elle passe. | ||
| 8 | + | ||
| 9 | +## Ce que contient ce dépôt | ||
| 10 | + | ||
| 11 | +``` | ||
| 12 | +main.go les options, le terminal et le câblage | ||
| 13 | +internal/gololang tout ce qui fait de cet éditeur Turbo Golo | ||
| 14 | + gololang.go le profil : nom, menu, serveur, où golo est installé | ||
| 15 | + scan.go le répartiteur du scanner, les commentaires, ce qui franchit une ligne | ||
| 16 | + literals.go les trois formes entre guillemets — "…", """…""" et '…' | ||
| 17 | + words.go les nombres, les mots-clés, les 157 builtins, les conventions de nommage | ||
| 18 | + templates.go trois déclarations //go:embed | ||
| 19 | + *.toml.tmpl les trois fichiers de départ d'un projet, embarqués | ||
| 20 | +``` | ||
| 21 | + | ||
| 22 | +Un millier de lignes environ en comptant les commentaires, dont quelque six cents pour le scanner — moins de cinq cents lignes de code au compte de qlty, et un tiers de celles-ci est la table des builtins. Il n'y a pas d'`internal/app`, pas d'`internal/ui`, pas d'`internal/buffer` : ceux-là existent une fois, dans la bibliothèque, et les six éditeurs les utilisent tels quels. | ||
| 23 | + | ||
| 24 | +## Ce que fait `main` | ||
| 25 | + | ||
| 26 | +Six choses, dans cet ordre : | ||
| 27 | + | ||
| 28 | +1. Il lit les options. | ||
| 29 | +2. Il appelle `gololang.Register()`, qui apprend à la bibliothèque à colorer les fichiers `.golo` et les scripts dont la première ligne nomme `golo`. | ||
| 30 | +3. Il construit `gololang.Profile()` — la valeur qui dit que cet éditeur est Turbo Golo. | ||
| 31 | +4. Il lit `.turbo-golo/settings.toml` dans le répertoire courant, s'il existe. | ||
| 32 | +5. Il ouvre le terminal et confie l'écran, le nom du thème et le profil à `app.New`. | ||
| 33 | +6. Il démarre `golo lsp` dans le répertoire du fichier édité, et lance la boucle d'événements. | ||
| 34 | + | ||
| 35 | +C'est toute la commande. Chaque décision qu'elle prend — quel thème l'emporte, quels fichiers ouvrir, faut-il démarrer un serveur de langage — porte sur *cette exécution*, pas sur Golo. | ||
| 36 | + | ||
| 37 | +## Le profil est la couture | ||
| 38 | + | ||
| 39 | +```go | ||
| 40 | +profile.Profile{ | ||
| 41 | + Name: "Turbo Golo", | ||
| 42 | + Slug: "turbo-golo", | ||
| 43 | + Language: "Golo", | ||
| 44 | + ToolsMenu: "~G~olo", | ||
| 45 | + RootMarkers: nil, | ||
| 46 | + Server: profile.Server{Command: "golo", Args: []string{"lsp"}, …}, | ||
| 47 | + Templates: profile.Templates{Settings: …, Snippets: …, Tools: …}, | ||
| 48 | +} | ||
| 49 | +``` | ||
| 50 | + | ||
| 51 | +Tout ce qui serait sinon un `"turbo-golo"`, un `"golo"` ou un `".golo"` codé en dur quelque part dans onze mille lignes est un champ ici. La bibliothèque les lit ; rien dans la bibliothèque ne sait ce qu'ils signifient. | ||
| 52 | + | ||
| 53 | +`Slug` porte plus qu'il n'y paraît. Le binaire s'appelle `turbo-golo`, le répertoire de projet `.turbo-golo`, la configuration personnelle vit dans `~/.config/turbo-golo`, et les variables d'environnement qui la remplacent sont `TURBO_GOLO_THEME_DIR` et `TURBO_GOLO_SNIPPET_DIR` — toutes dérivées de ce seul mot. | ||
| 54 | + | ||
| 55 | +`RootMarkers` est le seul champ vide ici et rempli chez tous les frères. Go a `go.mod`, Rust `Cargo.toml`, Python `pyproject.toml`, MoonBit `moon.mod` ; Golo n'a aucun manifeste. Un script est un fichier et un programme est un répertoire de fichiers, il n'y a donc rien vers quoi remonter, et le `ProjectRoot` de la bibliothèque — sans marqueur — répond par le répertoire du fichier. C'est aussi tout ce dont `golo lsp` a besoin : il répond à propos du fichier qu'on lui donne et résout les imports depuis les modules embarqués dans le binaire, jamais depuis le disque. | ||
| 56 | + | ||
| 57 | +## Pourquoi le scanner est ici et pas dans la bibliothèque | ||
| 58 | + | ||
| 59 | +turbo-core colore huit langages lui-même : TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell. Ce sont ceux que tout éditeur rencontre quel que soit son langage — la configuration d'un projet est en TOML ou en YAML, sa documentation en Markdown, ses scripts en shell, la construction de son image dans un Dockerfile. | ||
| 60 | + | ||
| 61 | +Golo n'en fait pas partie, ni Go, ni Rust, ni Python, ni MoonBit. Le langage qui *définit* un éditeur est enregistré par cet éditeur, et c'est pourquoi un fichier `.mbt` s'ouvre en texte brut ici et un fichier `.golo` s'ouvre en texte brut dans Turbo MoonBit. | ||
| 62 | + | ||
| 63 | +Cela aurait pu aller dans l'autre sens. Mettre les six scanners dans la bibliothèque permettrait à n'importe quel éditeur de colorer n'importe lequel de ces langages, sans coût en dépendances — un scanner Golo est du Go ordinaire. L'idée a été rejetée parce que la bibliothèque grossirait d'un langage chaque fois que quelqu'un construit un éditeur, et parce que « qu'est-ce que cet éditeur enregistre ? » cesserait d'être la première question à poser sur un nouveau venu. | ||
| 64 | + | ||
| 65 | +## Pourquoi le scanner n'a pas été emprunté à GoloScript | ||
| 66 | + | ||
| 67 | +GoloScript est écrit en Go, et son paquet `lexer` est exactement le tokeniseur que ce scanner réimplémente. Turbo Go se sert de `go/scanner` dans la même situation, la question est donc légitime. | ||
| 68 | + | ||
| 69 | +La réponse tient au nom du module. Le `go.mod` de GoloScript déclare `module golo`, un nom nu sans hôte, et le système de modules de Go ne sait pas aller chercher un module portant un tel chemin où que ce soit : importer `golo/lexer` depuis un autre module exige une directive `replace` pointant vers un dépôt cloné à côté, et un `replace` committé est précisément ce que `01-release.tag.sh` refuse de publier. Les règles du lexer sont donc reportées ici plutôt qu'appelées — et `lexer/lexer.go` et `token/token.go` sont la spécification contre laquelle le scanner est écrit, ligne pour ligne là où cela compte. La [page sur la coloration](colouring-and-completion.md) nomme les endroits où cette spécification et le parseur de l'interpréteur ne sont pas d'accord. | ||
| 70 | + | ||
| 71 | +## Pourquoi le menu de l'outillage s'appelle `~G~olo` et non `golo`, `gogolo` ou `wagolo` | ||
| 72 | + | ||
| 73 | +La touche chaude était la partie facile. Neuf lettres sont prises par les menus fixes — F, E, S, R, C, O, W, N et H — et `G` n'en fait pas partie, elle tombe donc sur la première lettre du mot, ce qui ne coûte à personne un second regard. Turbo Rust n'a pas eu cette chance et a fini sur `Rus~t~`. | ||
| 74 | + | ||
| 75 | +Le nom était la vraie décision, et elle a été prise comme chez tous les frères. Le menu contient ce que le projet a mis dans son fichier d'outils, et ce n'est pas toujours l'interpréteur : GoloScript lui-même, c'est trois binaires — `golo`, `gogolo`, `wagolo` — et le premier fichier d'outils qu'on écrit dépasse les trois, parce que les commandes d'un projet comprennent des conteneurs, des bases de données et une cible de `Makefile` ajoutée en 2019. Un menu appelé **golo** qui contient `wagolo build` est déjà un petit mensonge, et un qui contient `docker compose up` en est un gros. `Golo` est le langage, et le langage est ce pour quoi cet éditeur existe. | ||
| 76 | + | ||
| 77 | +## Pourquoi les tests pilotent le vrai éditeur | ||
| 78 | + | ||
| 79 | +`internal/gololang/editor_test.go` construit un Turbo Golo entier sur un terminal simulé — `app.New(screen, "turbo-classic", gololang.Profile())` — ouvre un fichier et vérifie la coloration, la barre de menus et les touches chaudes. Il n'utilise que l'API publique de la bibliothèque. | ||
| 80 | + | ||
| 81 | +C'est délibéré. La suite de la bibliothèque prouve que la bibliothèque fonctionne ; ce que ces tests prouvent, c'est que *cet éditeur est correctement assemblé* — que `Register` a été appelé, que le profil a atteint la barre de menus, qu'un fichier `.golo` sort coloré et qu'un script avec un shebang `golo` aussi. Un bogue où `main` oublierait d'enregistrer Golo passerait tous les tests de turbo-core. | ||
| 82 | + | ||
| 83 | +Le même fichier pilote un **vrai `golo lsp`** de bout en bout, dix fois. Il écrit un script, l'ouvre, démarre le serveur, puis : | ||
| 84 | + | ||
| 85 | +- **tape une déclaration de fonction qui n'existe que dans le tampon**, puis tape ses premières lettres sur une autre ligne et demande une complétion — `golo lsp` propose les mots-clés et les builtins pour n'importe quel fichier, une complétion contenant `println` ne prouverait donc rien ; une qui contient une fonction absente du disque prouve que le tampon a été envoyé ; | ||
| 86 | +- demande la **complétion d'un préfixe vide** et compare la réponse aux tables du scanner dans les deux sens — chaque mot-clé et chaque builtin que le scanner colore doit être proposé par le serveur, et tout ce que le serveur propose doit être connu du scanner, ce qui tient la table de 157 builtins à l'interpréteur plutôt qu'à la mémoire ; | ||
| 87 | +- demande la **définition** d'un appel et le **survol** de celui-ci, qui revient avec le commentaire `#` écrit au-dessus de la déclaration ; | ||
| 88 | +- demande les **symboles du fichier** ; | ||
| 89 | +- demande les **références** d'un appel — la déclaration et chaque appel dans le fichier — et son **implémentation**, qui est la déclaration elle-même, Golo n'ayant pas d'interfaces ; | ||
| 90 | +- cherche un **symbole dans le projet**, y compris dans un fichier que l'éditeur n'a jamais ouvert ; | ||
| 91 | +- ouvre un fichier qui **ne parse pas** et attend qu'un diagnostic arrive sans qu'on le demande — la seule fonctionnalité dont l'échec ressemble exactement au succès, parce qu'un éditeur qui n'a aucune erreur à montrer et un éditeur qui ne trouve pas l'erreur ont la même gouttière vide ; | ||
| 92 | +- ouvre un fichier contenant un **commentaire `//` à la C** et attend le lint qui dit que Golo utilise `#`. | ||
| 93 | + | ||
| 94 | +Un dernier test épingle ce que `golo lsp` *ne sait toujours pas* faire : il n'annonce pas `typeDefinition`, la documentation le dit, et le test échoue si un futur golo se met à répondre — la page est alors revue plutôt que de vieillir en silence. C'est ainsi que les trois tests qui le précèdent sont nés : jusqu'à GoloScript v0.2.0 le même test épinglait les références, les implémentations et les symboles du projet comme refus, et il est passé au rouge le jour où le serveur les a appris. | ||
| 95 | + | ||
| 96 | +## Alternatives rejetées | ||
| 97 | + | ||
| 98 | +**Forker Turbo MoonBit.** La façon évidente d'obtenir un cinquième éditeur, et la raison pour laquelle la bibliothèque existe à la place : cinq copies de onze mille lignes divergent en un mois, et chaque correction doit être faite cinq fois par quelqu'un qui se souvient qu'il y en a cinq. | ||
| 99 | + | ||
| 100 | +**Un système de plugins.** Turbo Golo est un programme Go qui importe une bibliothèque. Il n'y a ni chargement dynamique ni ABI. En ajouter un reviendrait à figer l'API de chaque paquet de turbo-core plutôt que celle des quelques-uns qu'un profil touche. | ||
| 101 | + | ||
| 102 | +**Un fichier de configuration au lieu d'un profil.** Le profil aurait pu être du TOML lu au démarrage, ce qui ferait d'un nouvel éditeur un fichier plutôt qu'un programme. Cela rendrait aussi le scanner inexprimable, et un éditeur à moitié configurable — tout sauf la coloration — est pire que l'une ou l'autre des réponses entières. | ||
| 103 | + | ||
| 104 | +**Importer le lexer de GoloScript.** Pesé plus haut : le module ne peut pas être récupéré, et un `replace` ne peut pas être publié. | ||
| 105 | + | ||
| 106 | +## Liens avec le reste | ||
| 107 | + | ||
| 108 | +- Ce que fait chaque paquet de la bibliothèque : [la référence des paquets de turbo-core](https://rickub.com/turbo-editors/turbo-core/blob/main/docs/fr/reference/packages.md) | ||
| 109 | +- Comment la coloration fonctionne ici : [Coloration et complétion](colouring-and-completion.md) | ||
| 110 | +- Pourquoi le menu des outils est une donnée : [Outils Golo](golo-tools.md) | ||
| 111 | +- Les décisions qui ont survécu au refactoring : [Décisions de conception](design-decisions.md) | ||
added
docs/fr/explanation/colouring-and-completion.md +118 -0 | new file mode 100644 | ||
| @@ -0,0 +1,118 @@ | ||
| 1 | +# Coloration et complétion — explication | |
| 2 | + | |
| 3 | +## De quoi s'agit-il ? | |
| 4 | + | |
| 5 | +Les deux fonctionnalités qui font de Turbo Golo un éditeur *pour Golo* plutôt qu'un éditeur de texte qui ouvre des fichiers `.golo` : la coloration syntaxique, et la complétion venue d'un serveur de langage. Elles fonctionnent très différemment, et la différence est instructive. | |
| 6 | + | |
| 7 | +## La coloration est à nous ; la complétion ne l'est pas | |
| 8 | + | |
| 9 | +La coloration est faite ici, dans quelque six cents lignes de Go écrites à la main. La complétion est faite par `golo lsp` — l'interpréteur lui-même, en mode serveur de langage — et Turbo Golo ne fait que demander et dessiner. | |
| 10 | + | |
| 11 | +Cette séparation n'est pas un accident d'effort. La coloration doit être **instantanée et tolérante** : elle s'exécute à chaque frappe, sur un texte invalide la plupart du temps pendant qu'on le tape, et un colorateur qui s'arrête pour réfléchir ou renonce devant une entrée cassée est pire que pas de colorateur. La complétion doit être **juste**, ce qui pour Golo signifie parser le fichier, suivre ses lignes `import` dans les modules embarqués dans le binaire, et savoir ce que prend chacun des 157 builtins — et rien de ce qui doit être instantané ne peut aussi être cela. | |
| 12 | + | |
| 13 | +L'éditeur dessine donc des couleurs qu'il a calculées lui-même, et affiche des complétions que quelqu'un d'autre a calculées. | |
| 14 | + | |
| 15 | +## Pourquoi Golo est scanné à la main, avec un lexer à portée de main | |
| 16 | + | |
| 17 | +GoloScript est écrit en Go, et son paquet `lexer` est un tokeniseur pour exactement ce langage. Turbo Go passe par `go/scanner` dans la même situation — la bibliothèque standard qui analyse son propre langage, si bien que l'éditeur et le compilateur s'accordent sur ce qu'est un token sans rien à maintenir en phase. Le geste évident était d'importer `golo/lexer` et de convertir ses positions avec le `LineIndex` de la bibliothèque. | |
| 18 | + | |
| 19 | +On ne peut pas l'importer. Le `go.mod` de GoloScript dit `module golo` : un nom nu, sans hôte, et le système de modules de Go n'a aucun moyen d'aller chercher un module portant un tel chemin. L'importer exige une directive `replace` pointant vers un dépôt cloné à côté, et un `replace` committé casse tout clone qui n'a pas ce dépôt à côté — c'est pourquoi le script de release refuse d'en publier un. Vendre les deux paquets était l'autre voie, et cela reviendrait à une copie du lexer de quelqu'un d'autre qui cesse d'être le sien le jour où il change. | |
| 20 | + | |
| 21 | +Le lexer est donc la **spécification** plutôt qu'une dépendance. `lexer/lexer.go` et `token/token.go` disent ce qu'est un token — quelles runes ouvrent un commentaire, comment un nombre se termine, quels mots sont réservés — et ce scanner dit la même chose dans le style `LineScanner` de la bibliothèque. Là où les deux pourraient diverger, la ligne du lexer est citée dans le code à côté de la décision. | |
| 22 | + | |
| 23 | +Ce sera donc le scanner. Quelque six cents lignes, un fichier chacun pour le répartiteur, les littéraux et les mots — et aucune tentative de moteur général. Il n'y a ni langage de motifs, ni format de grammaire, ni table d'expressions régulières : c'est du Go ordinaire qu'un lecteur peut suivre, la règle que suivent les huit scanners de turbo-core. | |
| 24 | + | |
| 25 | +## Ce qui franchit une ligne, et pourquoi c'est porté plutôt que coupé | |
| 26 | + | |
| 27 | +Quatre constructions peuvent courir d'une ligne à la suivante, et le lexer de l'interpréteur fait autorité sur chacune : | |
| 28 | + | |
| 29 | +- **Un commentaire bloc** court d'un `----` au suivant, où qu'il soit. Trois tirets sont deux signes moins et un troisième ; un cinquième tiret fait partie du texte. | |
| 30 | +- **Une chaîne** court jusqu'à son guillemet fermant. Le lexer la lit avec `for l.ch != '"' && l.ch != 0`, qui s'arrête au guillemet ou à la fin du fichier et à rien entre les deux — un retour à la ligne dans une chaîne fait partie de la chaîne. | |
| 31 | +- **Une chaîne multiligne `"""`** court jusqu'aux trois guillemets suivants, sans qu'aucun échappement soit considéré en chemin. | |
| 32 | +- **Un littéral de caractère `'…'`** est lu par la même boucle qu'une chaîne, et se comporte donc de la même façon. | |
| 33 | + | |
| 34 | +Tous les autres éditeurs de cette famille arrêtent un littéral à la fin de sa ligne quand le guillemet fermant manque, et Turbo MoonBit en fait une règle : la grammaire de MoonBit dit qu'un retour à la ligne avant le guillemet fermant est une *erreur*, il n'y a donc rien à porter. Le lexer de Golo dit le contraire, et le scanner suit le lexer : **une chaîne non terminée peint le reste du fichier jusqu'à ce qu'un guillemet se présente**, parce que c'est exactement ce que l'interpréteur lira comme chaîne. La couleur n'est pas un avertissement, c'est un énoncé sur ce que signifie le programme — et un écran de vert après un guillemet égaré est cet énoncé rendu visible. | |
| 35 | + | |
| 36 | +Ce qui est porté est une valeur qui dit *laquelle* des quatre est ouverte. Aucune ne s'imbrique, une profondeur serait donc une affirmation que le langage ne fait pas. | |
| 37 | + | |
| 38 | +## Où le scanner s'appuie sur le langage, et où sur la convention | |
| 39 | + | |
| 40 | +**Les mots-clés, les constantes et les builtins sont des tables lues dans GoloScript, pas remémorées.** Les 38 mots-clés sont la table de `token/token.go` moins les trois littéraux ; les 157 builtins sont ce que répond `evaluator.BuiltinNames()` moins les cinq compteurs de test qui commencent par un double souligné — les cinq mêmes que le serveur de langage tient hors de ses complétions. Un test tient cette table à un `golo lsp` en marche dans les deux sens, et c'est ainsi qu'elle reste une table lue dans l'outillage plutôt qu'une table que quelqu'un a tapée un jour. | |
| 41 | + | |
| 42 | +**Un nom qui commence par une majuscule est un type, et c'est ici une convention plutôt qu'une règle.** Golo n'a pas de règle de casse dans son lexer : `let Count = 1` est légal. Mais les structs, les unions et leurs variantes sont capitalisés par tout le monde — `Point`, `Shape`, `Circle`, `Some`, `None` — et rien d'autre ne l'est d'ordinaire, le scanner colore donc selon la convention, comme Turbo Python le fait avec la PEP 8. Ce que cela coûte : le constructeur d'une variante est coloré comme un type (`Circle(1.0)` et `Result_Failure("no")` ressemblent à des types appliqués à des arguments), et une variable capitalisée l'est aussi. Rien dans la syntaxe ne les sépare. | |
| 43 | + | |
| 44 | +**`Some`, `None`, `Ok` et `Err` sont ici des types, pas des constantes.** Turbo Rust et Turbo MoonBit les colorent comme des constantes parce qu'un lecteur les rencontre partout et les lit comme intégrés. En Golo ils ne le sont pas : ce sont les variantes d'unions ordinaires déclarées dans `gololang.Errors`, disponibles seulement après `import gololang.Errors`, et les colorer comme appartenant au langage dirait au lecteur qu'il n'a pas besoin d'un import alors qu'il en a besoin. | |
| 45 | + | |
| 46 | +**Le nom après `function` est une fonction, et le chemin après `module` ou `import` est un seul nom.** Partout ailleurs un nom est une fonction parce qu'une parenthèse le suit, et une déclaration — `function main = |args|` — est le seul endroit où ce n'est pas vrai ; sans cas particulier, chaque fonction qu'un fichier déclare serait colorée comme une variable ordinaire à l'endroit même où le lecteur la cherche. Un chemin de module — `hello.World`, `gololang.Errors` — est une seule portée et une seule couleur parce que c'est un seul nom, et la lecture dont il faut le sauver est celle où `gololang.Errors` ressemble à une variable à laquelle on fait quelque chose. | |
| 47 | + | |
| 48 | +**Un nom peut être presque n'importe quoi.** Le `isLetter` du lexer admet toute lettre ou marque Unicode, un souligné et quatre blocs d'emoji, si bien que `let 😀 = 1` et `function 🚀launch = …` sont du Golo légal. Le scanner utilise le même prédicat plutôt que celui, ASCII, de turbo-core, et ils sont donc colorés — comme `été` et `名前`, que Turbo MoonBit laisse sans couleur pour son propre langage. | |
| 49 | + | |
| 50 | +## Ce que le lexer lit et que le parseur refuse | |
| 51 | + | |
| 52 | +C'est la frontière qu'il vaut la peine d'énoncer clairement, parce que ce n'est pas une que le scanner peut voir. Le lexer et le parseur de GoloScript ont été écrits à des moments différents, et le lexer a de l'avance : il lit plusieurs tokens que le parseur, en v0.1.1, rejette ensuite. | |
| 53 | + | |
| 54 | +| Le lexer lit | Le parseur dit | | |
| 55 | +| --- | --- | | |
| 56 | +| `42L`, un long | `could not parse "42L" as integer` | | |
| 57 | +| `3.14F`, `2.0f`, un flottant | `could not parse "3.14F" as float` | | |
| 58 | +| `'x'`, un caractère | `no prefix parse function for CHAR found` | | |
| 59 | +| `1..3`, un intervalle | `expected next token to be ), got .. instead` | | |
| 60 | +| `orIfNull`, `oftype` | `expected next token to be ), got orIfNull instead` | | |
| 61 | +| `local function …` | `no prefix parse function for LOCAL found` | | |
| 62 | + | |
| 63 | +Le scanner colore ce que le lexer lit, parce que le lexer est la spécification de ce qu'*est* un token et que l'avis du parseur sur ce qu'il faut en faire peut changer demain. `42L` est donc un nombre et `orIfNull` un mot-clé, et **un token coloré n'est pas une promesse que l'interpréteur l'accepte**. Le serveur de langage vous le dit quand ce n'est pas le cas : ouvrez `demos/syntax-tour/lexer-only.golo` et chacune de ces lignes reçoit une marque dans la gouttière. | |
| 64 | + | |
| 65 | +## Ce que le scanner refuse de deviner | |
| 66 | + | |
| 67 | +Là où une construction ne peut pas être reconnue à partir de ce qu'une ligne contient, elle est laissée telle quelle plutôt qu'approximée. Un colorateur qui se trompe est pire qu'un colorateur qui se tait : | |
| 68 | + | |
| 69 | +| Non reconnu | Parce que | | |
| 70 | +| --- | --- | | |
| 71 | +| Les séparateurs de chiffres et les autres bases | Le lexer n'a ni `1_000`, ni `0xFF`, ni `0b1010`. `1_000` est le nombre `1` suivi du nom `_000`, et `0xFF` est `0` suivi de `xFF` — c'est ce que voit l'interpréteur, et colorer l'un ou l'autre comme un seul nombre inventerait un littéral qu'il rejette | | |
| 72 | +| Un point initial comme nombre | Le lexer exige un chiffre avant le point, `.5` est donc un point puis `5` | | |
| 73 | +| Un `l` minuscule comme suffixe long | Le lexer n'accepte que `L` ; `42l` est `42` et le nom `l` | | |
| 74 | +| Un mot-clé employé comme nom de méthode après un deux-points | `obj: match()` garde `match` en mot-clé. Le scanner ne suit pas ce qu'un deux-points introduit, et le lexer refuserait le mot de toute façon | | |
| 75 | +| Les échappements dans `"""…"""` | Le lexer ajoute chaque rune jusqu'aux trois guillemets, `"""a\"""` se termine donc au premier `"""` quoi que la barre oblique inverse ait voulu dire | | |
| 76 | +| Si un nom est lié dans cette portée | Rien ici ne lit plus d'une ligne à la fois ; c'est la question du serveur de langage, et [F1 y répond](../how-to/ask-about-code.md) | | |
| 77 | + | |
| 78 | +## Les huit autres langages viennent gratuitement | |
| 79 | + | |
| 80 | +TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell sont colorés par turbo-core, pas ici. Un projet Golo a un `README.md`, un `compose.yaml` pour le service auquel il parle, un Dockerfile pour être livré, et un éditeur qui ne colorerait que les fichiers `.golo` vous ferait le quitter pour le reste. | |
| 81 | + | |
| 82 | +Qu'ils soient partagés plutôt que copiés est tout l'intérêt de la bibliothèque : ils ont été écrits une fois, pour Turbo Go, et Turbo Golo les a obtenus en important un paquet. | |
| 83 | + | |
| 84 | +## La complétion, et pourquoi elle peut échouer en silence | |
| 85 | + | |
| 86 | +Turbo Golo ne sait rien de la sémantique de Golo et n'essaie pas. Il interroge `golo lsp` par le Language Server Protocol et dessine la réponse. | |
| 87 | + | |
| 88 | +Trois choses valent d'être sues à ce propos, parce que toutes trois ressemblent à « la complétion est cassée » : | |
| 89 | + | |
| 90 | +**Le serveur est l'interpréteur.** Il n'y a pas de binaire `golo-lsp` séparé à installer et aucun outillage dont il dépend : `golo lsp` réutilise le lexer, le parseur et l'AST de l'interpréteur. « Pas de complétion » sur une machine qui exécute des scripts Golo n'a donc qu'une seule cause — l'éditeur ne trouve pas `golo` — et la barre d'état le dit, avec l'adresse de la page des releases. | |
| 91 | + | |
| 92 | +**Seules les déclarations de premier niveau sont proposées.** La complétion du serveur liste les mots-clés, les builtins, les fonctions et unions déclarées au premier niveau du fichier, et les symboles apportés par `import` depuis les modules embarqués dans le binaire. Une fonction déclarée dans le corps d'une autre n'est pas dans la liste, ni rien qui vienne d'un fichier `.golo` à vous sur le disque : les imports de modules utilisateur ne sont pas résolus. C'est la conception du serveur, et elle est écrite ici plutôt que contournée. | |
| 93 | + | |
| 94 | +**Les diagnostics portent sur le parsing, pas sur l'exécution.** `golo lsp` publie les erreurs de syntaxe et deux lints — une confusion `:`/`.`, et les commentaires `//` ou `/* */` à la C là où Golo veut `#` et `----`. Un programme qui parse puis échoue à l'exécution ne reçoit aucune marque, parce que le serveur ne l'exécute jamais. Et une erreur de syntaxe surligne une ligne entière : les messages du parseur portent un numéro de ligne et pas de colonne, la marque tombe donc sur la ligne. | |
| 95 | + | |
| 96 | +La réponse de l'éditeur à la première est [Run ▸ Language server status](../reference/menus.md), qui dit ce qu'il a trouvé, où il l'a démarré et s'il est prêt — parce que « rien ne s'est passé » n'est pas quelque chose qu'un utilisateur peut traiter. | |
| 97 | + | |
| 98 | +## Neuf questions, une connexion — et les quatre auxquelles `golo lsp` ne répond pas | |
| 99 | + | |
| 100 | +La complétion est la chose la plus bruyante que fait le serveur de langage et la moins révélatrice. La même connexion pose huit autres questions, et elles se répartissent en trois sortes selon ce qui revient. | |
| 101 | + | |
| 102 | +**Quelque chose à lire.** `hover` — qu'est-ce que c'est ? — dessiné dans une boîte. Pour une fonction que vous avez déclarée, ce sont les commentaires `#` écrits juste au-dessus ; pour un builtin, sa signature et un exemple ; pour un mot-clé, une phrase. | |
| 103 | + | |
| 104 | +**Des endroits dans le code.** `definition`, `typeDefinition`, `implementation`, `references`. Une requête chacune, une seule forme de réponse à elles quatre, c'est pourquoi elles sont une seule fonction en dessous. Un seul endroit est ouvert ; plusieurs sont proposés en liste, parce qu'une réponse unique est l'exception plutôt que la règle — et pendant longtemps les éditeurs de cette famille prenaient le premier et jetaient le reste. | |
| 105 | + | |
| 106 | +**Des noms.** `documentSymbol` pour le plan d'un fichier, `workspace/symbol` pour une recherche dans tout le projet. Le protocole a trois formes pour un symbole et l'éditeur en veut une, l'aplatissement se fait donc là où les réponses arrivent plutôt que là où elles sont dessinées. | |
| 107 | + | |
| 108 | +Et une chose que personne ne demande : **`publishDiagnostics` arrive sans y être invité**, chaque fois que le serveur a un avis, à l'ouverture et à chaque modification. C'est pourquoi la marque dans la gouttière apparaît sans qu'on ait appuyé sur rien. | |
| 109 | + | |
| 110 | +**Une des neuf revient vide avec `golo lsp`, et c'est la frontière du serveur, pas celle de l'éditeur.** Il annonce `completion`, `hover`, `definition`, `documentSymbol`, `references`, `implementation` et `workspaceSymbol` — et pas `typeDefinition`, **Code ▸ Type definition** répond donc « rien trouvé ». Jusqu'à GoloScript v0.2.0 il n'annonçait que les quatre premiers, et **Shift-F12** (références), **Code ▸ Find implementations** et **Ctrl-T** (un symbole n'importe où dans le projet) revenaient vides aussi ; le test qui épingle cette frontière a échoué le jour où le serveur s'est mis à leur répondre, et ce paragraphe a été revu — c'est à cela que sert le test. La lacune est écrite plutôt que cachée parce que l'alternative — griser une entrée de menu selon ce qu'un serveur a dit au démarrage — donne au menu une forme différente selon la machine, et un utilisateur qui a lu cette page en sait plus qu'un qui a trouvé une entrée grisée. | |
| 111 | + | |
| 112 | +L'éditeur ne demande rien de tout cela avant que le serveur se dise prêt, et dit de quel cas il s'agit quand une question ne peut pas recevoir de réponse. « Rien trouvé » et « je n'ai pas fini de charger » sont la même réponse vide et des nouvelles très différentes ; les confondre est la façon la plus déroutante dont la complétion ait jamais échoué ici. | |
| 113 | + | |
| 114 | +## Liens avec le reste | |
| 115 | + | |
| 116 | +- Exactement ce qui est reconnu : [Langages colorés](../reference/languages.md) | |
| 117 | +- Faire fonctionner la complétion : [Activer la complétion Golo](../how-to/enable-completion.md) | |
| 118 | +- Où vit le scanner et pourquoi : [Architecture](architecture.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | +# Coloration et complétion — explication | ||
| 2 | + | ||
| 3 | +## De quoi s'agit-il ? | ||
| 4 | + | ||
| 5 | +Les deux fonctionnalités qui font de Turbo Golo un éditeur *pour Golo* plutôt qu'un éditeur de texte qui ouvre des fichiers `.golo` : la coloration syntaxique, et la complétion venue d'un serveur de langage. Elles fonctionnent très différemment, et la différence est instructive. | ||
| 6 | + | ||
| 7 | +## La coloration est à nous ; la complétion ne l'est pas | ||
| 8 | + | ||
| 9 | +La coloration est faite ici, dans quelque six cents lignes de Go écrites à la main. La complétion est faite par `golo lsp` — l'interpréteur lui-même, en mode serveur de langage — et Turbo Golo ne fait que demander et dessiner. | ||
| 10 | + | ||
| 11 | +Cette séparation n'est pas un accident d'effort. La coloration doit être **instantanée et tolérante** : elle s'exécute à chaque frappe, sur un texte invalide la plupart du temps pendant qu'on le tape, et un colorateur qui s'arrête pour réfléchir ou renonce devant une entrée cassée est pire que pas de colorateur. La complétion doit être **juste**, ce qui pour Golo signifie parser le fichier, suivre ses lignes `import` dans les modules embarqués dans le binaire, et savoir ce que prend chacun des 157 builtins — et rien de ce qui doit être instantané ne peut aussi être cela. | ||
| 12 | + | ||
| 13 | +L'éditeur dessine donc des couleurs qu'il a calculées lui-même, et affiche des complétions que quelqu'un d'autre a calculées. | ||
| 14 | + | ||
| 15 | +## Pourquoi Golo est scanné à la main, avec un lexer à portée de main | ||
| 16 | + | ||
| 17 | +GoloScript est écrit en Go, et son paquet `lexer` est un tokeniseur pour exactement ce langage. Turbo Go passe par `go/scanner` dans la même situation — la bibliothèque standard qui analyse son propre langage, si bien que l'éditeur et le compilateur s'accordent sur ce qu'est un token sans rien à maintenir en phase. Le geste évident était d'importer `golo/lexer` et de convertir ses positions avec le `LineIndex` de la bibliothèque. | ||
| 18 | + | ||
| 19 | +On ne peut pas l'importer. Le `go.mod` de GoloScript dit `module golo` : un nom nu, sans hôte, et le système de modules de Go n'a aucun moyen d'aller chercher un module portant un tel chemin. L'importer exige une directive `replace` pointant vers un dépôt cloné à côté, et un `replace` committé casse tout clone qui n'a pas ce dépôt à côté — c'est pourquoi le script de release refuse d'en publier un. Vendre les deux paquets était l'autre voie, et cela reviendrait à une copie du lexer de quelqu'un d'autre qui cesse d'être le sien le jour où il change. | ||
| 20 | + | ||
| 21 | +Le lexer est donc la **spécification** plutôt qu'une dépendance. `lexer/lexer.go` et `token/token.go` disent ce qu'est un token — quelles runes ouvrent un commentaire, comment un nombre se termine, quels mots sont réservés — et ce scanner dit la même chose dans le style `LineScanner` de la bibliothèque. Là où les deux pourraient diverger, la ligne du lexer est citée dans le code à côté de la décision. | ||
| 22 | + | ||
| 23 | +Ce sera donc le scanner. Quelque six cents lignes, un fichier chacun pour le répartiteur, les littéraux et les mots — et aucune tentative de moteur général. Il n'y a ni langage de motifs, ni format de grammaire, ni table d'expressions régulières : c'est du Go ordinaire qu'un lecteur peut suivre, la règle que suivent les huit scanners de turbo-core. | ||
| 24 | + | ||
| 25 | +## Ce qui franchit une ligne, et pourquoi c'est porté plutôt que coupé | ||
| 26 | + | ||
| 27 | +Quatre constructions peuvent courir d'une ligne à la suivante, et le lexer de l'interpréteur fait autorité sur chacune : | ||
| 28 | + | ||
| 29 | +- **Un commentaire bloc** court d'un `----` au suivant, où qu'il soit. Trois tirets sont deux signes moins et un troisième ; un cinquième tiret fait partie du texte. | ||
| 30 | +- **Une chaîne** court jusqu'à son guillemet fermant. Le lexer la lit avec `for l.ch != '"' && l.ch != 0`, qui s'arrête au guillemet ou à la fin du fichier et à rien entre les deux — un retour à la ligne dans une chaîne fait partie de la chaîne. | ||
| 31 | +- **Une chaîne multiligne `"""`** court jusqu'aux trois guillemets suivants, sans qu'aucun échappement soit considéré en chemin. | ||
| 32 | +- **Un littéral de caractère `'…'`** est lu par la même boucle qu'une chaîne, et se comporte donc de la même façon. | ||
| 33 | + | ||
| 34 | +Tous les autres éditeurs de cette famille arrêtent un littéral à la fin de sa ligne quand le guillemet fermant manque, et Turbo MoonBit en fait une règle : la grammaire de MoonBit dit qu'un retour à la ligne avant le guillemet fermant est une *erreur*, il n'y a donc rien à porter. Le lexer de Golo dit le contraire, et le scanner suit le lexer : **une chaîne non terminée peint le reste du fichier jusqu'à ce qu'un guillemet se présente**, parce que c'est exactement ce que l'interpréteur lira comme chaîne. La couleur n'est pas un avertissement, c'est un énoncé sur ce que signifie le programme — et un écran de vert après un guillemet égaré est cet énoncé rendu visible. | ||
| 35 | + | ||
| 36 | +Ce qui est porté est une valeur qui dit *laquelle* des quatre est ouverte. Aucune ne s'imbrique, une profondeur serait donc une affirmation que le langage ne fait pas. | ||
| 37 | + | ||
| 38 | +## Où le scanner s'appuie sur le langage, et où sur la convention | ||
| 39 | + | ||
| 40 | +**Les mots-clés, les constantes et les builtins sont des tables lues dans GoloScript, pas remémorées.** Les 38 mots-clés sont la table de `token/token.go` moins les trois littéraux ; les 157 builtins sont ce que répond `evaluator.BuiltinNames()` moins les cinq compteurs de test qui commencent par un double souligné — les cinq mêmes que le serveur de langage tient hors de ses complétions. Un test tient cette table à un `golo lsp` en marche dans les deux sens, et c'est ainsi qu'elle reste une table lue dans l'outillage plutôt qu'une table que quelqu'un a tapée un jour. | ||
| 41 | + | ||
| 42 | +**Un nom qui commence par une majuscule est un type, et c'est ici une convention plutôt qu'une règle.** Golo n'a pas de règle de casse dans son lexer : `let Count = 1` est légal. Mais les structs, les unions et leurs variantes sont capitalisés par tout le monde — `Point`, `Shape`, `Circle`, `Some`, `None` — et rien d'autre ne l'est d'ordinaire, le scanner colore donc selon la convention, comme Turbo Python le fait avec la PEP 8. Ce que cela coûte : le constructeur d'une variante est coloré comme un type (`Circle(1.0)` et `Result_Failure("no")` ressemblent à des types appliqués à des arguments), et une variable capitalisée l'est aussi. Rien dans la syntaxe ne les sépare. | ||
| 43 | + | ||
| 44 | +**`Some`, `None`, `Ok` et `Err` sont ici des types, pas des constantes.** Turbo Rust et Turbo MoonBit les colorent comme des constantes parce qu'un lecteur les rencontre partout et les lit comme intégrés. En Golo ils ne le sont pas : ce sont les variantes d'unions ordinaires déclarées dans `gololang.Errors`, disponibles seulement après `import gololang.Errors`, et les colorer comme appartenant au langage dirait au lecteur qu'il n'a pas besoin d'un import alors qu'il en a besoin. | ||
| 45 | + | ||
| 46 | +**Le nom après `function` est une fonction, et le chemin après `module` ou `import` est un seul nom.** Partout ailleurs un nom est une fonction parce qu'une parenthèse le suit, et une déclaration — `function main = |args|` — est le seul endroit où ce n'est pas vrai ; sans cas particulier, chaque fonction qu'un fichier déclare serait colorée comme une variable ordinaire à l'endroit même où le lecteur la cherche. Un chemin de module — `hello.World`, `gololang.Errors` — est une seule portée et une seule couleur parce que c'est un seul nom, et la lecture dont il faut le sauver est celle où `gololang.Errors` ressemble à une variable à laquelle on fait quelque chose. | ||
| 47 | + | ||
| 48 | +**Un nom peut être presque n'importe quoi.** Le `isLetter` du lexer admet toute lettre ou marque Unicode, un souligné et quatre blocs d'emoji, si bien que `let 😀 = 1` et `function 🚀launch = …` sont du Golo légal. Le scanner utilise le même prédicat plutôt que celui, ASCII, de turbo-core, et ils sont donc colorés — comme `été` et `名前`, que Turbo MoonBit laisse sans couleur pour son propre langage. | ||
| 49 | + | ||
| 50 | +## Ce que le lexer lit et que le parseur refuse | ||
| 51 | + | ||
| 52 | +C'est la frontière qu'il vaut la peine d'énoncer clairement, parce que ce n'est pas une que le scanner peut voir. Le lexer et le parseur de GoloScript ont été écrits à des moments différents, et le lexer a de l'avance : il lit plusieurs tokens que le parseur, en v0.1.1, rejette ensuite. | ||
| 53 | + | ||
| 54 | +| Le lexer lit | Le parseur dit | | ||
| 55 | +| --- | --- | | ||
| 56 | +| `42L`, un long | `could not parse "42L" as integer` | | ||
| 57 | +| `3.14F`, `2.0f`, un flottant | `could not parse "3.14F" as float` | | ||
| 58 | +| `'x'`, un caractère | `no prefix parse function for CHAR found` | | ||
| 59 | +| `1..3`, un intervalle | `expected next token to be ), got .. instead` | | ||
| 60 | +| `orIfNull`, `oftype` | `expected next token to be ), got orIfNull instead` | | ||
| 61 | +| `local function …` | `no prefix parse function for LOCAL found` | | ||
| 62 | + | ||
| 63 | +Le scanner colore ce que le lexer lit, parce que le lexer est la spécification de ce qu'*est* un token et que l'avis du parseur sur ce qu'il faut en faire peut changer demain. `42L` est donc un nombre et `orIfNull` un mot-clé, et **un token coloré n'est pas une promesse que l'interpréteur l'accepte**. Le serveur de langage vous le dit quand ce n'est pas le cas : ouvrez `demos/syntax-tour/lexer-only.golo` et chacune de ces lignes reçoit une marque dans la gouttière. | ||
| 64 | + | ||
| 65 | +## Ce que le scanner refuse de deviner | ||
| 66 | + | ||
| 67 | +Là où une construction ne peut pas être reconnue à partir de ce qu'une ligne contient, elle est laissée telle quelle plutôt qu'approximée. Un colorateur qui se trompe est pire qu'un colorateur qui se tait : | ||
| 68 | + | ||
| 69 | +| Non reconnu | Parce que | | ||
| 70 | +| --- | --- | | ||
| 71 | +| Les séparateurs de chiffres et les autres bases | Le lexer n'a ni `1_000`, ni `0xFF`, ni `0b1010`. `1_000` est le nombre `1` suivi du nom `_000`, et `0xFF` est `0` suivi de `xFF` — c'est ce que voit l'interpréteur, et colorer l'un ou l'autre comme un seul nombre inventerait un littéral qu'il rejette | | ||
| 72 | +| Un point initial comme nombre | Le lexer exige un chiffre avant le point, `.5` est donc un point puis `5` | | ||
| 73 | +| Un `l` minuscule comme suffixe long | Le lexer n'accepte que `L` ; `42l` est `42` et le nom `l` | | ||
| 74 | +| Un mot-clé employé comme nom de méthode après un deux-points | `obj: match()` garde `match` en mot-clé. Le scanner ne suit pas ce qu'un deux-points introduit, et le lexer refuserait le mot de toute façon | | ||
| 75 | +| Les échappements dans `"""…"""` | Le lexer ajoute chaque rune jusqu'aux trois guillemets, `"""a\"""` se termine donc au premier `"""` quoi que la barre oblique inverse ait voulu dire | | ||
| 76 | +| Si un nom est lié dans cette portée | Rien ici ne lit plus d'une ligne à la fois ; c'est la question du serveur de langage, et [F1 y répond](../how-to/ask-about-code.md) | | ||
| 77 | + | ||
| 78 | +## Les huit autres langages viennent gratuitement | ||
| 79 | + | ||
| 80 | +TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell sont colorés par turbo-core, pas ici. Un projet Golo a un `README.md`, un `compose.yaml` pour le service auquel il parle, un Dockerfile pour être livré, et un éditeur qui ne colorerait que les fichiers `.golo` vous ferait le quitter pour le reste. | ||
| 81 | + | ||
| 82 | +Qu'ils soient partagés plutôt que copiés est tout l'intérêt de la bibliothèque : ils ont été écrits une fois, pour Turbo Go, et Turbo Golo les a obtenus en important un paquet. | ||
| 83 | + | ||
| 84 | +## La complétion, et pourquoi elle peut échouer en silence | ||
| 85 | + | ||
| 86 | +Turbo Golo ne sait rien de la sémantique de Golo et n'essaie pas. Il interroge `golo lsp` par le Language Server Protocol et dessine la réponse. | ||
| 87 | + | ||
| 88 | +Trois choses valent d'être sues à ce propos, parce que toutes trois ressemblent à « la complétion est cassée » : | ||
| 89 | + | ||
| 90 | +**Le serveur est l'interpréteur.** Il n'y a pas de binaire `golo-lsp` séparé à installer et aucun outillage dont il dépend : `golo lsp` réutilise le lexer, le parseur et l'AST de l'interpréteur. « Pas de complétion » sur une machine qui exécute des scripts Golo n'a donc qu'une seule cause — l'éditeur ne trouve pas `golo` — et la barre d'état le dit, avec l'adresse de la page des releases. | ||
| 91 | + | ||
| 92 | +**Seules les déclarations de premier niveau sont proposées.** La complétion du serveur liste les mots-clés, les builtins, les fonctions et unions déclarées au premier niveau du fichier, et les symboles apportés par `import` depuis les modules embarqués dans le binaire. Une fonction déclarée dans le corps d'une autre n'est pas dans la liste, ni rien qui vienne d'un fichier `.golo` à vous sur le disque : les imports de modules utilisateur ne sont pas résolus. C'est la conception du serveur, et elle est écrite ici plutôt que contournée. | ||
| 93 | + | ||
| 94 | +**Les diagnostics portent sur le parsing, pas sur l'exécution.** `golo lsp` publie les erreurs de syntaxe et deux lints — une confusion `:`/`.`, et les commentaires `//` ou `/* */` à la C là où Golo veut `#` et `----`. Un programme qui parse puis échoue à l'exécution ne reçoit aucune marque, parce que le serveur ne l'exécute jamais. Et une erreur de syntaxe surligne une ligne entière : les messages du parseur portent un numéro de ligne et pas de colonne, la marque tombe donc sur la ligne. | ||
| 95 | + | ||
| 96 | +La réponse de l'éditeur à la première est [Run ▸ Language server status](../reference/menus.md), qui dit ce qu'il a trouvé, où il l'a démarré et s'il est prêt — parce que « rien ne s'est passé » n'est pas quelque chose qu'un utilisateur peut traiter. | ||
| 97 | + | ||
| 98 | +## Neuf questions, une connexion — et les quatre auxquelles `golo lsp` ne répond pas | ||
| 99 | + | ||
| 100 | +La complétion est la chose la plus bruyante que fait le serveur de langage et la moins révélatrice. La même connexion pose huit autres questions, et elles se répartissent en trois sortes selon ce qui revient. | ||
| 101 | + | ||
| 102 | +**Quelque chose à lire.** `hover` — qu'est-ce que c'est ? — dessiné dans une boîte. Pour une fonction que vous avez déclarée, ce sont les commentaires `#` écrits juste au-dessus ; pour un builtin, sa signature et un exemple ; pour un mot-clé, une phrase. | ||
| 103 | + | ||
| 104 | +**Des endroits dans le code.** `definition`, `typeDefinition`, `implementation`, `references`. Une requête chacune, une seule forme de réponse à elles quatre, c'est pourquoi elles sont une seule fonction en dessous. Un seul endroit est ouvert ; plusieurs sont proposés en liste, parce qu'une réponse unique est l'exception plutôt que la règle — et pendant longtemps les éditeurs de cette famille prenaient le premier et jetaient le reste. | ||
| 105 | + | ||
| 106 | +**Des noms.** `documentSymbol` pour le plan d'un fichier, `workspace/symbol` pour une recherche dans tout le projet. Le protocole a trois formes pour un symbole et l'éditeur en veut une, l'aplatissement se fait donc là où les réponses arrivent plutôt que là où elles sont dessinées. | ||
| 107 | + | ||
| 108 | +Et une chose que personne ne demande : **`publishDiagnostics` arrive sans y être invité**, chaque fois que le serveur a un avis, à l'ouverture et à chaque modification. C'est pourquoi la marque dans la gouttière apparaît sans qu'on ait appuyé sur rien. | ||
| 109 | + | ||
| 110 | +**Une des neuf revient vide avec `golo lsp`, et c'est la frontière du serveur, pas celle de l'éditeur.** Il annonce `completion`, `hover`, `definition`, `documentSymbol`, `references`, `implementation` et `workspaceSymbol` — et pas `typeDefinition`, **Code ▸ Type definition** répond donc « rien trouvé ». Jusqu'à GoloScript v0.2.0 il n'annonçait que les quatre premiers, et **Shift-F12** (références), **Code ▸ Find implementations** et **Ctrl-T** (un symbole n'importe où dans le projet) revenaient vides aussi ; le test qui épingle cette frontière a échoué le jour où le serveur s'est mis à leur répondre, et ce paragraphe a été revu — c'est à cela que sert le test. La lacune est écrite plutôt que cachée parce que l'alternative — griser une entrée de menu selon ce qu'un serveur a dit au démarrage — donne au menu une forme différente selon la machine, et un utilisateur qui a lu cette page en sait plus qu'un qui a trouvé une entrée grisée. | ||
| 111 | + | ||
| 112 | +L'éditeur ne demande rien de tout cela avant que le serveur se dise prêt, et dit de quel cas il s'agit quand une question ne peut pas recevoir de réponse. « Rien trouvé » et « je n'ai pas fini de charger » sont la même réponse vide et des nouvelles très différentes ; les confondre est la façon la plus déroutante dont la complétion ait jamais échoué ici. | ||
| 113 | + | ||
| 114 | +## Liens avec le reste | ||
| 115 | + | ||
| 116 | +- Exactement ce qui est reconnu : [Langages colorés](../reference/languages.md) | ||
| 117 | +- Faire fonctionner la complétion : [Activer la complétion Golo](../how-to/enable-completion.md) | ||
| 118 | +- Où vit le scanner et pourquoi : [Architecture](architecture.md) | ||
added
docs/fr/explanation/design-decisions.md +123 -0 | new file mode 100644 | ||
| @@ -0,0 +1,123 @@ | ||
| 1 | +# Décisions de conception — explication | |
| 2 | + | |
| 3 | +## De quoi s'agit-il ? | |
| 4 | + | |
| 5 | +Les choix qui ont façonné Turbo Golo, quelles étaient les alternatives, et pourquoi elles ont été écartées. C'est la page à lire avant de modifier quelque chose qui paraît arbitraire. | |
| 6 | + | |
| 7 | +La plupart de ces décisions appartiennent à turbo-core, la bibliothèque sur laquelle les six éditeurs de la famille sont bâtis : Turbo Golo en hérite, et cette page les raconte parce qu'elles expliquent ce que vous voyez à l'écran. Ce qui n'appartient qu'à Turbo Golo — le langage, le serveur, les fichiers de départ — est signalé comme tel. | |
| 8 | + | |
| 9 | +## Deux dépendances, pas une de plus | |
| 10 | + | |
| 11 | +Turbo Golo ne dépend directement que de turbo-core et de `tcell/v2`, et turbo-core lui-même n'y ajoute que `BurntSushi/toml`. Tout le reste est la bibliothèque standard — y compris le tokeniseur, le client JSON-RPC, le cadrage LSP et la gestion des fichiers. | |
| 12 | + | |
| 13 | +**Ce qui a été écarté.** `go.lsp.dev/jsonrpc2` aurait économisé peut-être trois cents lignes du paquet `lsp` de turbo-core. `rivo/tview` en aurait économisé bien davantage dans `ui`. Une bibliothèque de coloration syntaxique aurait apporté cinquante langages au lieu de neuf. | |
| 14 | + | |
| 15 | +**Pourquoi.** Un éditeur est un programme qu'on garde des années et qu'on modifie souvent. Chaque dépendance en est un morceau qu'on ne peut pas modifier, pas tester entièrement, et qu'il faut suivre. Le protocole est assez simple pour être écrit, et l'avoir écrit a placé toute la conversation à un endroit qu'un lecteur peut suivre. Trois cents lignes qu'on comprend valent mieux que trois cents qu'on hérite. | |
| 16 | + | |
| 17 | +L'exception confirme la règle : `tcell` n'est pas une commodité, c'est la base de compatibilité des terminaux, et la réimplémenter ne serait ni un petit travail ni un travail honnête. | |
| 18 | + | |
| 19 | +## Le framework de widgets est écrit à la main | |
| 20 | + | |
| 21 | +`tview` a des widgets. `bubbletea` a une architecture. Aucun des deux n'a ce qu'avait Turbo Vision : des fenêtres déplaçables qui se recouvrent avec des ombres, une barre de menus à lettres d'accès et des dialogues modaux, le tout dessiné en caractères semi-graphiques sur seize couleurs. | |
| 22 | + | |
| 23 | +L'architecture à la Elm de `bubbletea` redessine toute la vue à chaque message. Ce modèle est excellent pour un formulaire et malcommode pour un éditeur plein écran où les fenêtres s'empilent et où le curseur doit se trouver dans une cellule précise. | |
| 24 | + | |
| 25 | +Écrire le framework a coûté environ mille cinq cents lignes. En échange, l'éditeur ressemble à Turbo C plutôt qu'à une interface moderne portant un fond bleu, et chaque décision d'affichage se trouve à un fichier de distance. | |
| 26 | + | |
| 27 | +## Les rectangles sont en coordonnées écran absolues | |
| 28 | + | |
| 29 | +Le `Bounds()` de chaque widget indique où il se trouve réellement sur le terminal, pas où il se trouve par rapport à son parent. Tester si un clic l'atteint est alors un simple test de rectangle, et aucun événement n'a jamais besoin d'être traduit en descendant. | |
| 30 | + | |
| 31 | +**Le coût** est que les conteneurs placent leurs enfants dans l'espace de l'écran. **L'alternative** — des coordonnées relatives avec une traduction à chaque saut — déplace le calcul de la mise en page vers la gestion des événements, où il est fait bien plus souvent et où il est bien plus facile de se tromper. Le découpage se compose quand même correctement, puisqu'un peintre intersecte le découpage de son parent : un enfant dont le calcul est faux ne dessine rien plutôt que de dessiner par-dessus ses voisins. | |
| 32 | + | |
| 33 | +## Toute modification passe par une seule fonction | |
| 34 | + | |
| 35 | +`buffer.ReplaceRange` est le seul endroit où le texte est modifié. Insertion, retour arrière, suppression, indentation, collage et annulation y convergent tous, et c'est le seul endroit où sont maintenus l'historique d'annulation, le drapeau « modifié », le compteur de révision et le curseur. | |
| 36 | + | |
| 37 | +L'alternative — chaque opération tenant sa propre comptabilité — est la façon dont naissent les bugs d'annulation. Il y a exactement une chose à réussir, et elle est testée directement. | |
| 38 | + | |
| 39 | +## Les fenêtres suivent le terminal, elles ne s'y mettent pas à l'échelle | |
| 40 | + | |
| 41 | +Une fenêtre a un **mode de croissance**, qui nomme les bords du bureau qu'elle suit. Une fenêtre de document suit les bords droit et bas : son coin supérieur gauche reste où il est, et son coin opposé se déplace exactement autant que celui du terminal. Une fenêtre qui remplissait le terminal le remplit donc toujours, et une fenêtre que vous aviez décalée garde son décalage. | |
| 42 | + | |
| 43 | +**L'alternative était la mise à l'échelle proportionnelle** — multiplier le rectangle de chaque fenêtre par le rapport des tailles. Elle a été écartée parce qu'elle déplace des fenêtres que l'utilisateur a placées exprès, et parce que les arrondis la rendent destructive : réduisez puis agrandissez, et plus rien n'est où il était. Turbo Vision utilisait des modes de croissance, et c'est toujours la bonne réponse. | |
| 44 | + | |
| 45 | +Quel que soit son mode, une fenêtre est ensuite bornée à la taille du bureau. Une fenêtre plus grande que le bureau qui la contient a des parties que personne ne peut atteindre. | |
| 46 | + | |
| 47 | +## Les cases d'une fenêtre disent ce qu'elles vont faire, pas ce que la fenêtre est | |
| 48 | + | |
| 49 | +Le cadre porte deux cases : `[x]` à gauche ferme la fenêtre, `[■]` à droite lui donne tout le bureau. | |
| 50 | + | |
| 51 | +La case de fermeture était `[■]` — celle de Turbo Vision — et il fallait qu'elle bouge. Deux cases sur un même cadre doivent se distinguer d'un coup d'œil, et un bloc plein se lit bien plus volontiers « remplir l'écran » que « fermer ». `[x]` veut dire fermer depuis trente ans ; le bloc est allé au travail auquel il ressemble. | |
| 52 | + | |
| 53 | +La case d'agrandissement **change avec l'état de la fenêtre** : `[■]` tant qu'il reste de la place, `[▬]` une fois que la fenêtre remplit le bureau. L'autre solution était un symbole fixe, et elle rend le bouton ambigu précisément au moment où l'on en a besoin : on voit bien que la fenêtre est grande, mais pas si l'actionner va l'agrandir encore ou la remettre en place. Un contrôle qui montre son *état* laisse déduire l'action ; un contrôle qui montre son *action*, non. | |
| 54 | + | |
| 55 | +Une fenêtre qui n'a nulle part où s'agrandir n'affiche **aucune case**, plutôt qu'une case sans effet. Seul le bureau sait quelle surface une fenêtre remplirait : une fenêtre qui n'est sur aucun bureau n'a rien à proposer. | |
| 56 | + | |
| 57 | +**Window ▸ Maximise est le même bascule**, pas une action à sens unique. Un menu et un bouton en désaccord sur le sens d'« agrandir » seraient un bug qu'on signale, pas une subtilité qu'on apprécie. | |
| 58 | + | |
| 59 | +## L'annulation fusionne les séries de frappe | |
| 60 | + | |
| 61 | +Taper `function` puis Ctrl-Z retire les huit lettres. Une série de retours arrière aussi. Déplacer le curseur clôt la série, et la frappe ne fusionne jamais avec l'effacement. | |
| 62 | + | |
| 63 | +L'annulation caractère par caractère est ce que donne une implémentation naïve, et c'est ce que faisait Turbo C lui-même. C'est aussi ce dont plus personne ne veut. | |
| 64 | + | |
| 65 | +## Les thèmes sont en TOML, avec deux formes d'héritage | |
| 66 | + | |
| 67 | +**Entre fichiers**, `inherits` prend les styles résolus du parent comme point de départ. Un thème à vous peut donc tenir en cinq lignes. | |
| 68 | + | |
| 69 | +**Entre clés**, le long des points : `syntax.keyword` retombe sur `syntax`, et `syntax` sur `default`. Cela se produit deux fois — une fois à l'analyse, pour qu'une entrée ne définissant que `fg` hérite de son `bg`, et une fois à la lecture, pour qu'un thème qui ne mentionne jamais `syntax.keyword` colore quand même les mots-clés. | |
| 70 | + | |
| 71 | +C'est cette seconde forme qui fait qu'un thème partiel est un thème utilisable, et c'est pourquoi il n'existe pas de thème laissant la moitié de l'écran non peinte. | |
| 72 | + | |
| 73 | +**Pourquoi TOML plutôt que JSON.** Les commentaires. Un thème est un fichier que l'on modifie à la main et que l'on annote. | |
| 74 | + | |
| 75 | +**Une couleur inconnue est une erreur**, pas un repli silencieux sur la couleur par défaut du terminal. Une faute de frappe qui repeint discrètement la moitié de l'écran est bien plus difficile à trouver qu'une qui le dit au chargement. | |
| 76 | + | |
| 77 | +## Le serveur de langage est optionnel par construction | |
| 78 | + | |
| 79 | +`app.Language` enveloppe toute la conversation avec `golo lsp`, et lorsqu'il n'y a pas de serveur, chaque méthode ne fait rien plutôt que d'échouer. Rien d'autre dans l'éditeur ne se demande si un serveur de langage existe. | |
| 80 | + | |
| 81 | +L'alternative — vérifier `nil` à chacun des vingt points d'appel — offre vingt occasions d'oublier. Ici, oublier est impossible : il n'y a rien à vérifier. | |
| 82 | + | |
| 83 | +C'est pourquoi `golo` n'est ni embarqué, ni téléchargé, ni requis. Il est cherché dans le `PATH` puis dans `/usr/local/bin`, là où l'installeur de GoloScript le dépose, et son absence est signalée sur la barre d'état avec l'adresse d'où on l'obtient. | |
| 84 | + | |
| 85 | +## Le serveur est l'interpréteur lui-même | |
| 86 | + | |
| 87 | +C'est une décision de Turbo Golo, pas de la bibliothèque. Le serveur de langage n'est pas un programme à part : `golo lsp` met le même binaire qui exécute un script en mode serveur, avec son lexeur, son analyseur et son arbre syntaxique. Une machine qui exécute du Golo complète donc du Golo, et il n'y a rien de plus à installer. | |
| 88 | + | |
| 89 | +Le prix est que l'éditeur n'obtient que ce que l'interpréteur sait dire : la complétion, la description d'un symbole, sa déclaration, ses références et son implémentation dans le fichier, le plan du fichier, la recherche d'un symbole dans tout le projet et les diagnostics — huit des neuf questions que turbo-core sait poser, plus les problèmes que le serveur publie de lui-même. La définition d'un type répond qu'il n'y a rien ; jusqu'à GoloScript v0.2.0, les références, les implémentations et la recherche dans le projet en faisaient autant. C'est écrit dans la [référence des menus](../reference/menus.md) plutôt que masqué derrière des entrées grisées, et un test le vérifie pour qu'un `golo` qui apprendrait ces questions soit remarqué — c'est exactement ainsi que les trois qu'il a apprises l'ont été. | |
| 90 | + | |
| 91 | +## Aucun marqueur de projet | |
| 92 | + | |
| 93 | +turbo-core sait remonter depuis le fichier ouvert jusqu'à un fichier marqueur pour donner une racine à un serveur de langage, et les éditeurs dont le langage a un manifeste s'en servent. Turbo Golo n'en déclare aucun et ne remonte vers rien : Golo n'a pas de manifeste de projet — un script est un fichier, un programme est un dossier de fichiers — et le serveur reçoit le dossier du fichier ouvert, qui est aussi tout ce dont il a besoin, puisque `golo lsp` répond sur le fichier qu'on lui donne et résout les imports depuis les modules embarqués dans le binaire, jamais depuis le disque. | |
| 94 | + | |
| 95 | +Un marqueur inventé aurait fait ressembler la remontée à une règle là où il n'y en a pas. | |
| 96 | + | |
| 97 | +## L'enregistrement est atomique, et fidèle à l'octet près | |
| 98 | + | |
| 99 | +Un enregistrement écrit dans un fichier temporaire du même répertoire puis le renomme sur la cible, en conservant les permissions d'origine. Un enregistrement interrompu ne peut pas laisser un fichier source à moitié écrit. | |
| 100 | + | |
| 101 | +Par ailleurs, les fins de ligne avec lesquelles un fichier a été lu et son saut de ligne final — ou son absence — sont mémorisés : ouvrir puis enregistrer un fichier non modifié le reproduit octet pour octet. Un éditeur qui normalise silencieusement les fins de ligne transforme une modification d'une ligne en un diff du fichier entier. | |
| 102 | + | |
| 103 | +## Le presse-papier est celui de l'éditeur | |
| 104 | + | |
| 105 | +Un programme en terminal ne peut pas lire le presse-papier du système de façon portable. Plutôt que de faire semblant, Turbo Golo partage un presse-papier entre ses propres fenêtres, ce que faisait Turbo C. | |
| 106 | + | |
| 107 | +## La version est une propriété du build, pas des sources | |
| 108 | + | |
| 109 | +La version était autrefois `const Version = "0.1.0"` dans le source de l'éditeur. Elle était juste le jour où elle a été écrite et fausse pendant les quatorze commits suivants, parce que rien dans le fait de valider, taguer ou installer ne touche à une constante Go. Une boîte About, c'est ce que l'on regarde au moment de signaler un bug ; un numéro qui y nomme une release que le binaire n'est pas est pire que pas de numéro, parce qu'on le croit. | |
| 110 | + | |
| 111 | +Le numéro est donc pris au build. L'éditeur de liens estampille `git describe --tags --dirty` dans le paquet `version` de turbo-core depuis le Makefile et depuis l'installeur, ce qui fait que `make install` produit un éditeur qui nomme le commit dont il vient. Quand rien ne l'a estampillé, le binaire interroge `runtime/debug.ReadBuildInfo()`, qui couvre le seul chemin impossible à estampiller : `go install rickub.com/turbo-editors/turbo-golo@v0.2.0`, où aucun Makefile n'intervient et où l'outil Go connaît la version du module. Ce n'est que si les deux se taisent qu'il annonce `unknown` — délibérément pas un numéro, puisque l'échec contre lequel tout ceci est conçu est justement une version vraisemblable que personne n'a posée. | |
| 112 | + | |
| 113 | +Deux limites du système de build expliquent le reste de la conception. **Il ne lit pas les tags git**, donc un `go build .` nu ne pourra jamais annoncer `0.1.0-14-g88a4c38`, si astucieux que soit le code ; il annonce `devel` plus le commit, et la documentation le dit plutôt que de laisser croire que tous les builds se valent. Et ce qu'il annonce *effectivement* pour un tel build est une **pseudo-version** — `v0.1.1-0.20260831165958-88a4c3859bf3` — affichée comme `devel` à la place, parce que son `0.1.1` est un correctif qui n'existe pas et serait lu comme tel. | |
| 114 | + | |
| 115 | +`vcs.time` est délibérément inutilisé. C'est l'horodatage du commit, et tout binaire est lié après le commit dont il provient : l'étiqueter « Built » serait faux sur chacun d'eux. Une date de build ne s'affiche que si un build en a réellement estampillé une, la même règle que suit la boîte About de bout en bout : **un fait que personne n'a enregistré n'a pas de ligne**, plutôt qu'une ligne vide qui se lit comme un échec à la remplir. | |
| 116 | + | |
| 117 | +Rejeté : une cible `make release` qui tague, construit et pousse. Publier tient en trois commandes git, et les emballer masque laquelle a échoué ; l'estampillage de la version était la partie qu'on ne pouvait pas faire à la main de façon fiable, et c'est celle qui a été automatisée. | |
| 118 | + | |
| 119 | +## Liens avec le reste | |
| 120 | + | |
| 121 | +- Ce que sont les paquets et comment ils s'articulent : [Architecture](architecture.md) | |
| 122 | +- Comment fonctionnent la coloration et la complétion : [Coloration et complétion](colouring-and-completion.md) | |
| 123 | +- Ce que le serveur sait et ne sait pas répondre : [Outils Golo](golo-tools.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,123 @@ | |||
| 1 | +# Décisions de conception — explication | ||
| 2 | + | ||
| 3 | +## De quoi s'agit-il ? | ||
| 4 | + | ||
| 5 | +Les choix qui ont façonné Turbo Golo, quelles étaient les alternatives, et pourquoi elles ont été écartées. C'est la page à lire avant de modifier quelque chose qui paraît arbitraire. | ||
| 6 | + | ||
| 7 | +La plupart de ces décisions appartiennent à turbo-core, la bibliothèque sur laquelle les six éditeurs de la famille sont bâtis : Turbo Golo en hérite, et cette page les raconte parce qu'elles expliquent ce que vous voyez à l'écran. Ce qui n'appartient qu'à Turbo Golo — le langage, le serveur, les fichiers de départ — est signalé comme tel. | ||
| 8 | + | ||
| 9 | +## Deux dépendances, pas une de plus | ||
| 10 | + | ||
| 11 | +Turbo Golo ne dépend directement que de turbo-core et de `tcell/v2`, et turbo-core lui-même n'y ajoute que `BurntSushi/toml`. Tout le reste est la bibliothèque standard — y compris le tokeniseur, le client JSON-RPC, le cadrage LSP et la gestion des fichiers. | ||
| 12 | + | ||
| 13 | +**Ce qui a été écarté.** `go.lsp.dev/jsonrpc2` aurait économisé peut-être trois cents lignes du paquet `lsp` de turbo-core. `rivo/tview` en aurait économisé bien davantage dans `ui`. Une bibliothèque de coloration syntaxique aurait apporté cinquante langages au lieu de neuf. | ||
| 14 | + | ||
| 15 | +**Pourquoi.** Un éditeur est un programme qu'on garde des années et qu'on modifie souvent. Chaque dépendance en est un morceau qu'on ne peut pas modifier, pas tester entièrement, et qu'il faut suivre. Le protocole est assez simple pour être écrit, et l'avoir écrit a placé toute la conversation à un endroit qu'un lecteur peut suivre. Trois cents lignes qu'on comprend valent mieux que trois cents qu'on hérite. | ||
| 16 | + | ||
| 17 | +L'exception confirme la règle : `tcell` n'est pas une commodité, c'est la base de compatibilité des terminaux, et la réimplémenter ne serait ni un petit travail ni un travail honnête. | ||
| 18 | + | ||
| 19 | +## Le framework de widgets est écrit à la main | ||
| 20 | + | ||
| 21 | +`tview` a des widgets. `bubbletea` a une architecture. Aucun des deux n'a ce qu'avait Turbo Vision : des fenêtres déplaçables qui se recouvrent avec des ombres, une barre de menus à lettres d'accès et des dialogues modaux, le tout dessiné en caractères semi-graphiques sur seize couleurs. | ||
| 22 | + | ||
| 23 | +L'architecture à la Elm de `bubbletea` redessine toute la vue à chaque message. Ce modèle est excellent pour un formulaire et malcommode pour un éditeur plein écran où les fenêtres s'empilent et où le curseur doit se trouver dans une cellule précise. | ||
| 24 | + | ||
| 25 | +Écrire le framework a coûté environ mille cinq cents lignes. En échange, l'éditeur ressemble à Turbo C plutôt qu'à une interface moderne portant un fond bleu, et chaque décision d'affichage se trouve à un fichier de distance. | ||
| 26 | + | ||
| 27 | +## Les rectangles sont en coordonnées écran absolues | ||
| 28 | + | ||
| 29 | +Le `Bounds()` de chaque widget indique où il se trouve réellement sur le terminal, pas où il se trouve par rapport à son parent. Tester si un clic l'atteint est alors un simple test de rectangle, et aucun événement n'a jamais besoin d'être traduit en descendant. | ||
| 30 | + | ||
| 31 | +**Le coût** est que les conteneurs placent leurs enfants dans l'espace de l'écran. **L'alternative** — des coordonnées relatives avec une traduction à chaque saut — déplace le calcul de la mise en page vers la gestion des événements, où il est fait bien plus souvent et où il est bien plus facile de se tromper. Le découpage se compose quand même correctement, puisqu'un peintre intersecte le découpage de son parent : un enfant dont le calcul est faux ne dessine rien plutôt que de dessiner par-dessus ses voisins. | ||
| 32 | + | ||
| 33 | +## Toute modification passe par une seule fonction | ||
| 34 | + | ||
| 35 | +`buffer.ReplaceRange` est le seul endroit où le texte est modifié. Insertion, retour arrière, suppression, indentation, collage et annulation y convergent tous, et c'est le seul endroit où sont maintenus l'historique d'annulation, le drapeau « modifié », le compteur de révision et le curseur. | ||
| 36 | + | ||
| 37 | +L'alternative — chaque opération tenant sa propre comptabilité — est la façon dont naissent les bugs d'annulation. Il y a exactement une chose à réussir, et elle est testée directement. | ||
| 38 | + | ||
| 39 | +## Les fenêtres suivent le terminal, elles ne s'y mettent pas à l'échelle | ||
| 40 | + | ||
| 41 | +Une fenêtre a un **mode de croissance**, qui nomme les bords du bureau qu'elle suit. Une fenêtre de document suit les bords droit et bas : son coin supérieur gauche reste où il est, et son coin opposé se déplace exactement autant que celui du terminal. Une fenêtre qui remplissait le terminal le remplit donc toujours, et une fenêtre que vous aviez décalée garde son décalage. | ||
| 42 | + | ||
| 43 | +**L'alternative était la mise à l'échelle proportionnelle** — multiplier le rectangle de chaque fenêtre par le rapport des tailles. Elle a été écartée parce qu'elle déplace des fenêtres que l'utilisateur a placées exprès, et parce que les arrondis la rendent destructive : réduisez puis agrandissez, et plus rien n'est où il était. Turbo Vision utilisait des modes de croissance, et c'est toujours la bonne réponse. | ||
| 44 | + | ||
| 45 | +Quel que soit son mode, une fenêtre est ensuite bornée à la taille du bureau. Une fenêtre plus grande que le bureau qui la contient a des parties que personne ne peut atteindre. | ||
| 46 | + | ||
| 47 | +## Les cases d'une fenêtre disent ce qu'elles vont faire, pas ce que la fenêtre est | ||
| 48 | + | ||
| 49 | +Le cadre porte deux cases : `[x]` à gauche ferme la fenêtre, `[■]` à droite lui donne tout le bureau. | ||
| 50 | + | ||
| 51 | +La case de fermeture était `[■]` — celle de Turbo Vision — et il fallait qu'elle bouge. Deux cases sur un même cadre doivent se distinguer d'un coup d'œil, et un bloc plein se lit bien plus volontiers « remplir l'écran » que « fermer ». `[x]` veut dire fermer depuis trente ans ; le bloc est allé au travail auquel il ressemble. | ||
| 52 | + | ||
| 53 | +La case d'agrandissement **change avec l'état de la fenêtre** : `[■]` tant qu'il reste de la place, `[▬]` une fois que la fenêtre remplit le bureau. L'autre solution était un symbole fixe, et elle rend le bouton ambigu précisément au moment où l'on en a besoin : on voit bien que la fenêtre est grande, mais pas si l'actionner va l'agrandir encore ou la remettre en place. Un contrôle qui montre son *état* laisse déduire l'action ; un contrôle qui montre son *action*, non. | ||
| 54 | + | ||
| 55 | +Une fenêtre qui n'a nulle part où s'agrandir n'affiche **aucune case**, plutôt qu'une case sans effet. Seul le bureau sait quelle surface une fenêtre remplirait : une fenêtre qui n'est sur aucun bureau n'a rien à proposer. | ||
| 56 | + | ||
| 57 | +**Window ▸ Maximise est le même bascule**, pas une action à sens unique. Un menu et un bouton en désaccord sur le sens d'« agrandir » seraient un bug qu'on signale, pas une subtilité qu'on apprécie. | ||
| 58 | + | ||
| 59 | +## L'annulation fusionne les séries de frappe | ||
| 60 | + | ||
| 61 | +Taper `function` puis Ctrl-Z retire les huit lettres. Une série de retours arrière aussi. Déplacer le curseur clôt la série, et la frappe ne fusionne jamais avec l'effacement. | ||
| 62 | + | ||
| 63 | +L'annulation caractère par caractère est ce que donne une implémentation naïve, et c'est ce que faisait Turbo C lui-même. C'est aussi ce dont plus personne ne veut. | ||
| 64 | + | ||
| 65 | +## Les thèmes sont en TOML, avec deux formes d'héritage | ||
| 66 | + | ||
| 67 | +**Entre fichiers**, `inherits` prend les styles résolus du parent comme point de départ. Un thème à vous peut donc tenir en cinq lignes. | ||
| 68 | + | ||
| 69 | +**Entre clés**, le long des points : `syntax.keyword` retombe sur `syntax`, et `syntax` sur `default`. Cela se produit deux fois — une fois à l'analyse, pour qu'une entrée ne définissant que `fg` hérite de son `bg`, et une fois à la lecture, pour qu'un thème qui ne mentionne jamais `syntax.keyword` colore quand même les mots-clés. | ||
| 70 | + | ||
| 71 | +C'est cette seconde forme qui fait qu'un thème partiel est un thème utilisable, et c'est pourquoi il n'existe pas de thème laissant la moitié de l'écran non peinte. | ||
| 72 | + | ||
| 73 | +**Pourquoi TOML plutôt que JSON.** Les commentaires. Un thème est un fichier que l'on modifie à la main et que l'on annote. | ||
| 74 | + | ||
| 75 | +**Une couleur inconnue est une erreur**, pas un repli silencieux sur la couleur par défaut du terminal. Une faute de frappe qui repeint discrètement la moitié de l'écran est bien plus difficile à trouver qu'une qui le dit au chargement. | ||
| 76 | + | ||
| 77 | +## Le serveur de langage est optionnel par construction | ||
| 78 | + | ||
| 79 | +`app.Language` enveloppe toute la conversation avec `golo lsp`, et lorsqu'il n'y a pas de serveur, chaque méthode ne fait rien plutôt que d'échouer. Rien d'autre dans l'éditeur ne se demande si un serveur de langage existe. | ||
| 80 | + | ||
| 81 | +L'alternative — vérifier `nil` à chacun des vingt points d'appel — offre vingt occasions d'oublier. Ici, oublier est impossible : il n'y a rien à vérifier. | ||
| 82 | + | ||
| 83 | +C'est pourquoi `golo` n'est ni embarqué, ni téléchargé, ni requis. Il est cherché dans le `PATH` puis dans `/usr/local/bin`, là où l'installeur de GoloScript le dépose, et son absence est signalée sur la barre d'état avec l'adresse d'où on l'obtient. | ||
| 84 | + | ||
| 85 | +## Le serveur est l'interpréteur lui-même | ||
| 86 | + | ||
| 87 | +C'est une décision de Turbo Golo, pas de la bibliothèque. Le serveur de langage n'est pas un programme à part : `golo lsp` met le même binaire qui exécute un script en mode serveur, avec son lexeur, son analyseur et son arbre syntaxique. Une machine qui exécute du Golo complète donc du Golo, et il n'y a rien de plus à installer. | ||
| 88 | + | ||
| 89 | +Le prix est que l'éditeur n'obtient que ce que l'interpréteur sait dire : la complétion, la description d'un symbole, sa déclaration, ses références et son implémentation dans le fichier, le plan du fichier, la recherche d'un symbole dans tout le projet et les diagnostics — huit des neuf questions que turbo-core sait poser, plus les problèmes que le serveur publie de lui-même. La définition d'un type répond qu'il n'y a rien ; jusqu'à GoloScript v0.2.0, les références, les implémentations et la recherche dans le projet en faisaient autant. C'est écrit dans la [référence des menus](../reference/menus.md) plutôt que masqué derrière des entrées grisées, et un test le vérifie pour qu'un `golo` qui apprendrait ces questions soit remarqué — c'est exactement ainsi que les trois qu'il a apprises l'ont été. | ||
| 90 | + | ||
| 91 | +## Aucun marqueur de projet | ||
| 92 | + | ||
| 93 | +turbo-core sait remonter depuis le fichier ouvert jusqu'à un fichier marqueur pour donner une racine à un serveur de langage, et les éditeurs dont le langage a un manifeste s'en servent. Turbo Golo n'en déclare aucun et ne remonte vers rien : Golo n'a pas de manifeste de projet — un script est un fichier, un programme est un dossier de fichiers — et le serveur reçoit le dossier du fichier ouvert, qui est aussi tout ce dont il a besoin, puisque `golo lsp` répond sur le fichier qu'on lui donne et résout les imports depuis les modules embarqués dans le binaire, jamais depuis le disque. | ||
| 94 | + | ||
| 95 | +Un marqueur inventé aurait fait ressembler la remontée à une règle là où il n'y en a pas. | ||
| 96 | + | ||
| 97 | +## L'enregistrement est atomique, et fidèle à l'octet près | ||
| 98 | + | ||
| 99 | +Un enregistrement écrit dans un fichier temporaire du même répertoire puis le renomme sur la cible, en conservant les permissions d'origine. Un enregistrement interrompu ne peut pas laisser un fichier source à moitié écrit. | ||
| 100 | + | ||
| 101 | +Par ailleurs, les fins de ligne avec lesquelles un fichier a été lu et son saut de ligne final — ou son absence — sont mémorisés : ouvrir puis enregistrer un fichier non modifié le reproduit octet pour octet. Un éditeur qui normalise silencieusement les fins de ligne transforme une modification d'une ligne en un diff du fichier entier. | ||
| 102 | + | ||
| 103 | +## Le presse-papier est celui de l'éditeur | ||
| 104 | + | ||
| 105 | +Un programme en terminal ne peut pas lire le presse-papier du système de façon portable. Plutôt que de faire semblant, Turbo Golo partage un presse-papier entre ses propres fenêtres, ce que faisait Turbo C. | ||
| 106 | + | ||
| 107 | +## La version est une propriété du build, pas des sources | ||
| 108 | + | ||
| 109 | +La version était autrefois `const Version = "0.1.0"` dans le source de l'éditeur. Elle était juste le jour où elle a été écrite et fausse pendant les quatorze commits suivants, parce que rien dans le fait de valider, taguer ou installer ne touche à une constante Go. Une boîte About, c'est ce que l'on regarde au moment de signaler un bug ; un numéro qui y nomme une release que le binaire n'est pas est pire que pas de numéro, parce qu'on le croit. | ||
| 110 | + | ||
| 111 | +Le numéro est donc pris au build. L'éditeur de liens estampille `git describe --tags --dirty` dans le paquet `version` de turbo-core depuis le Makefile et depuis l'installeur, ce qui fait que `make install` produit un éditeur qui nomme le commit dont il vient. Quand rien ne l'a estampillé, le binaire interroge `runtime/debug.ReadBuildInfo()`, qui couvre le seul chemin impossible à estampiller : `go install rickub.com/turbo-editors/turbo-golo@v0.2.0`, où aucun Makefile n'intervient et où l'outil Go connaît la version du module. Ce n'est que si les deux se taisent qu'il annonce `unknown` — délibérément pas un numéro, puisque l'échec contre lequel tout ceci est conçu est justement une version vraisemblable que personne n'a posée. | ||
| 112 | + | ||
| 113 | +Deux limites du système de build expliquent le reste de la conception. **Il ne lit pas les tags git**, donc un `go build .` nu ne pourra jamais annoncer `0.1.0-14-g88a4c38`, si astucieux que soit le code ; il annonce `devel` plus le commit, et la documentation le dit plutôt que de laisser croire que tous les builds se valent. Et ce qu'il annonce *effectivement* pour un tel build est une **pseudo-version** — `v0.1.1-0.20260831165958-88a4c3859bf3` — affichée comme `devel` à la place, parce que son `0.1.1` est un correctif qui n'existe pas et serait lu comme tel. | ||
| 114 | + | ||
| 115 | +`vcs.time` est délibérément inutilisé. C'est l'horodatage du commit, et tout binaire est lié après le commit dont il provient : l'étiqueter « Built » serait faux sur chacun d'eux. Une date de build ne s'affiche que si un build en a réellement estampillé une, la même règle que suit la boîte About de bout en bout : **un fait que personne n'a enregistré n'a pas de ligne**, plutôt qu'une ligne vide qui se lit comme un échec à la remplir. | ||
| 116 | + | ||
| 117 | +Rejeté : une cible `make release` qui tague, construit et pousse. Publier tient en trois commandes git, et les emballer masque laquelle a échoué ; l'estampillage de la version était la partie qu'on ne pouvait pas faire à la main de façon fiable, et c'est celle qui a été automatisée. | ||
| 118 | + | ||
| 119 | +## Liens avec le reste | ||
| 120 | + | ||
| 121 | +- Ce que sont les paquets et comment ils s'articulent : [Architecture](architecture.md) | ||
| 122 | +- Comment fonctionnent la coloration et la complétion : [Coloration et complétion](colouring-and-completion.md) | ||
| 123 | +- Ce que le serveur sait et ne sait pas répondre : [Outils Golo](golo-tools.md) | ||
added
docs/fr/explanation/golo-tools.md +119 -0 | new file mode 100644 | ||
| @@ -0,0 +1,119 @@ | ||
| 1 | +# Outils Golo — explication | |
| 2 | + | |
| 3 | +## De quoi s'agit-il ? | |
| 4 | + | |
| 5 | +Un menu **Golo** dont les commandes viennent d'un fichier TOML, chacune exécutée là où l'outil l'a demandé — un popup, une fenêtre de terminal ou une fenêtre d'édition — et les fichiers ouverts relus ensuite. Cette page explique pourquoi chacun de ces choix est ce qu'il est. | |
| 6 | + | |
| 7 | +## Pourquoi la sortie a trois destinations, et un popup par défaut | |
| 8 | + | |
| 9 | +La première version de ce mécanisme, dans Turbo Go, mettait chaque commande dans une fenêtre de terminal, et c'était le mauvais défaut pour la plupart d'entre elles. | |
| 10 | + | |
| 11 | +Un terminal est la bonne réponse quand le programme est *interactif ou long* : `golo main.golo` sur un script qui lit le clavier avec `readln` doit pouvoir recevoir une réponse, et un script qui sert du HTTP avec `httpServe` doit pouvoir être interrompu par `Ctrl-C`. Rien de cela n'est vrai de `golo --test`, qui imprime son rapport et se termine. Lui donner une fenêtre entière — qu'il faut ensuite fermer, sur un bureau où les fenêtres se chevauchent et sont numérotées — c'est plus de cérémonie que le résultat ne mérite. | |
| 12 | + | |
| 13 | +Un popup est la bonne réponse pour une commande qu'on lance, qu'on lit et qu'on écarte. Il est modal, ce qui est un vrai coût et est nommé dans le [guide](../how-to/run-golo-commands.md) : un `gogolo build` dont vous n'attendiez pas la lenteur — il lance le compilateur Go — retient l'éditeur jusqu'à ce qu'il finisse ou que vous appuyiez sur Échap. Ce coût a été accepté à dessein, parce que l'alternative — un dialogue qui surgit trois secondes plus tard — avale ce qu'on était en train de taper à l'instant où il arrive. | |
| 14 | + | |
| 15 | +Le popup **s'ouvre donc immédiatement et se remplit**. Vous voyez la progression, rien ne vous surprend, et Échap le ferme et arrête la commande, ce qui est la seule façon d'interrompre quelque chose dont la sortie n'est pas dans un terminal. | |
| 16 | + | |
| 17 | +Une fenêtre d'édition est la bonne réponse pour une sortie que vous allez parcourir : un long rapport de tests, ou le source Go que `gogolo transpile` imprime. C'est un tampon ordinaire, `Ctrl-F` le fouille et `Save as` le conserve. Elle est remplie une fois la commande terminée plutôt qu'au fil de l'eau, parce qu'un tampon qui grandit sous le curseur pendant qu'on le fouille est le contraire de ce pour quoi ce mode existe. | |
| 18 | + | |
| 19 | +Aucune des trois ne convient à tout, c'est pourquoi `output` est dans le fichier plutôt que dans le code. `Run` est l'exemple travaillé : c'est la première commande du fichier de départ, elle dit `terminal`, et le commentaire à côté dit pourquoi. `Debug` aussi — le débogueur pas à pas lit le clavier — et `REPL`, qui n'est rien d'autre qu'un clavier. | |
| 20 | + | |
| 21 | +## Pourquoi Run vient en premier | |
| 22 | + | |
| 23 | +Dans le fichier de départ de Turbo MoonBit le premier outil est `moon check`, parce que pour un langage compilé « est-ce correct ? » est la question posée le plus souvent et celle qui ne produit rien. Golo est un langage de script, et la question posée le plus souvent est « qu'est-ce que ça imprime ? ». La première ligne du menu Golo exécute donc le fichier, et la deuxième lance les tests. | |
| 24 | + | |
| 25 | +## Pourquoi une fenêtre de terminal est toujours là | |
| 26 | + | |
| 27 | +L'éditeur en avait déjà une — un vrai pseudo-terminal avec un émulateur VT, construit pour les fenêtres `F8` — si bien qu'`output = "terminal"` coûte un champ dans ses options et achète pour rien les couleurs, la pagination, `Ctrl-C`, la saisie clavier et le défilement arrière, parce que ce sont les mêmes mécanismes qu'utilise n'importe quel terminal. La sortie de GoloScript utilise ces couleurs : `golo --test` dessine des coches vertes, et la famille de builtins `uiPrint` dessine ce que le script a demandé. | |
| 28 | + | |
| 29 | +La fenêtre reste après la fin de la commande, et c'est le but : la sortie est ce que vous avez demandé, et une fenêtre qui disparaîtrait avec elle serait inutile. | |
| 30 | + | |
| 31 | +## Pourquoi le code de sortie est toujours dans le titre | |
| 32 | + | |
| 33 | +Un script qui se termine sans rien imprimer, ou `golo --test` sur un répertoire sans fichier de test, n'imprime rien du tout. Un popup au corps vide et au titre neutre ne se distingue pas d'un popup dont la commande n'a pas démarré, et le lecteur est laissé à deviner la seule chose qu'il voulait savoir. | |
| 34 | + | |
| 35 | +Le titre porte donc le verdict — `— ok` ou `— exit 1` — et un corps vide dit `(no output)` une fois la commande terminée. Pendant qu'elle tourne encore le corps reste vide, parce que « (no output) » est un verdict et qu'une commande en cours n'en a pas encore atteint. | |
| 36 | + | |
| 37 | +## Pourquoi les commandes sont dans un fichier | |
| 38 | + | |
| 39 | +Huit commandes câblées dans l'éditeur auraient répondu à la demande. Elles auraient aussi été fausses en une semaine. | |
| 40 | + | |
| 41 | +Chaque commande du fichier de départ passe par l'un des trois binaires de GoloScript — `golo`, `gogolo` ou `wagolo` — et aucune n'a donc besoin de quoi que ce soit au-delà de l'outillage lui-même. C'est un défaut défendable et ce n'est la réponse universelle de personne. Un projet à un seul point d'entrée veut `golo main.golo` sans qu'on lui demande quel script. Un projet livré en binaire natif veut `gogolo build -o bin/app app.golo` avec la sortie fixée. Un projet qui vise le navigateur veut `wagolo build -target=js` et jamais `wasi`. Un projet qui tourne sous Docker veut `docker run … k33g/gololang`. Rien de cela n'est connaissable d'ici, et tout cela est une ligne dans un fichier. | |
| 42 | + | |
| 43 | +Les huit sont donc **des défauts, pas du code** : c'est le contenu du fichier de départ que **Golo ▸ Create tools file** écrit, et en changer une revient à éditer un fichier plutôt qu'à reconstruire un éditeur. Le fichier est lu à chaque ouverture du menu, pour la même raison que le menu Snippets : une modification doit prendre effet aussitôt, et le fichier est souvent ouvert dans la fenêtre derrière le menu. | |
| 44 | + | |
| 45 | +Les commandes vont à `sh -c` — `cmd.exe /S /C` sous Windows — plutôt que d'être découpées en argv ici. Le fichier appartient à l'utilisateur, les tubes, les globs et `&&` sont donc des fonctionnalités plutôt que des dangers, et une entrée peut être `golo --test && gogolo build -o app main.golo`. Découper un argv reviendrait à inventer des règles de citation pour une chaîne que quelqu'un a écrite à la main. | |
| 46 | + | |
| 47 | +## Pourquoi il n'y a pas de fichier d'outils au niveau utilisateur | |
| 48 | + | |
| 49 | +Les snippets sont lus dans deux fichiers — le vôtre et celui du projet — parce que vos snippets sont vos habitudes et doivent vous suivre d'un projet à l'autre. | |
| 50 | + | |
| 51 | +Les outils ne sont pas ainsi. Ils appartiennent à l'outillage propre d'un projet : un fichier d'outils global proposerait `golo --test` dans un dépôt qui n'a jamais entendu parler de Golo, et un projet qui ne fait qu'interpréter ses scripts aurait `wagolo build` dans son menu avec un TinyGo qu'il n'a jamais installé. Le fichier est par projet, et c'est toute la règle. | |
| 52 | + | |
| 53 | +## Pourquoi un outil peut nommer son propre menu | |
| 54 | + | |
| 55 | +Un menu appelé **Golo** qui contient `docker compose up` est un mensonge sur ce qu'est le menu. Le premier fichier d'outils qu'on écrit dépasse Golo, parce que les commandes d'un projet ne portent pas toutes sur le langage dans lequel il est écrit : conteneurs, bases de données, déploiements, une cible de `Makefile` ajoutée en 2019. | |
| 56 | + | |
| 57 | +Deux formes ont été envisagées. Un **second menu fixe** appelé Tools — tout ce qui est Golo dans Golo, tout le reste dans Tools — c'est une clé dans le format et aucun problème de nommage, mais cela ne fait que déplacer le mensonge : un menu Tools contenant `docker compose up`, `psql` et un script de déploiement est tout aussi indifférencié, et dès qu'il y a dix entrées personne n'en retrouve une. Et un **second fichier**, `menus.toml`, garde le fichier d'outils simple au prix de deux fichiers qui doivent s'accorder sur les outils existants. | |
| 58 | + | |
| 59 | +Le menu est donc un **nom libre sur l'outil**, dans le seul fichier : `menu = "Docker"`. Un nom que rien d'autre n'utilise crée le menu ; omettre la clé signifie Golo. Il n'y a pas de liste de noms autorisés, parce qu'une liste serait la liste des projets de quelqu'un d'autre. | |
| 60 | + | |
| 61 | +Golo lui-même reste fixe sur la barre plutôt que de devenir un nom de plus venu du fichier. **Golo ▸ Create tools file** doit être atteignable dans un projet qui n'a aucun fichier d'outils — c'est exactement le projet qui en a besoin — et un menu qui n'existe qu'une fois le fichier là ne peut pas proposer de l'écrire. | |
| 62 | + | |
| 63 | +## Pourquoi la touche chaude n'est pas au fichier de la choisir | |
| 64 | + | |
| 65 | +L'auteur d'un fichier d'outils ne peut pas savoir quelles lettres sont libres. Il voit `File`, `Edit`, `Search`, `Run`, `Code`, `Options`, `Window`, `Snippets`, `Golo` et `Help` sur la barre, mais seulement en comptant les soulignés, et un projet partagé entre plusieurs personnes dépendrait alors de ce que personne n'ajoute un menu qui entre en collision. | |
| 66 | + | |
| 67 | +Les collisions ici sont **silencieuses**, et c'est ce qui rend la conception nécessaire. La barre répond au premier menu dont la touche correspond ; un second menu revendiquant la même lettre n'est pas une erreur et se dessine normalement — il ne s'ouvre simplement jamais. Ce piège s'est déjà refermé une fois dans cette famille : `Snippets` et `Search` voulaient tous deux `S`, `Snippets` était l'inatteignable, et tous les tests passaient. La correction fut alors de déplacer Snippets sur `N` à la main. Laisser un fichier nommer des menus en fait un danger permanent plutôt qu'une erreur ponctuelle, l'attribution est donc faite par l'éditeur : la première lettre du nom que rien d'autre ne revendique. | |
| 68 | + | |
| 69 | +Les tildes écrits dans le nom sont honorés **quand la lettre est libre**, et remplacés sans bruit quand elle ne l'est pas. Refuser le fichier était l'alternative, et c'est pire : la collision dépend des menus existants, un fichier d'outils qui marchait casserait donc le jour où une version de l'éditeur ajoute un menu. Entre un menu sur une lettre que vous n'avez pas demandée et un menu que vous ne pouvez pas ouvrir, le premier est la moindre perte. | |
| 70 | + | |
| 71 | +Quand toutes les lettres d'un nom sont prises, le menu n'a aucune touche chaude. `F10`, les flèches et la souris l'atteignent toujours, et l'alternative — prendre une lettre qui n'est pas dans le nom — mettrait un souligné sous rien. | |
| 72 | + | |
| 73 | +## Pourquoi la barre est reconstruite à partir d'un stat | |
| 74 | + | |
| 75 | +`Menu.OnOpen` regarnit les entrées d'un menu juste avant qu'il se déroule, et c'est ainsi que les menus Golo et Snippets suivent leurs fichiers sans redémarrage. Cela ne peut pas aider ici : l'*ensemble* des menus fait partie de la barre, pas d'un menu en particulier, et ajouter `menu = "Docker"` au fichier doit mettre Docker sur la barre. | |
| 76 | + | |
| 77 | +Lire et parser le fichier à chaque tour de la boucle d'événements le ferait, et ce serait aussi du travail pour rien à chaque frappe dans un fichier que personne n'a modifié. La barre porte donc la taille et la date de modification du fichier d'outils à partir duquel elle a été construite, et un `stat` par tour décide s'il faut la reconstruire. Éditer le fichier dans la fenêtre devant soi, l'enregistrer, et voir la barre changer, c'est le cas pour lequel c'est fait. | |
| 78 | + | |
| 79 | +## Pourquoi les fichiers ouverts sont relus, et seulement certains | |
| 80 | + | |
| 81 | +Golo n'a pas de formateur, aucune commande de départ ne réécrit donc le fichier devant vous — mais `golo new` écrit un fichier dans le répertoire, `gogolo build -keep-go` laisse un `.go` à côté du script, et vos propres outils peuvent faire n'importe quoi. Sans rien de plus, l'éditeur resterait sur une copie périmée d'un fichier qu'une autre commande a changé, et le `F2` suivant écrirait votre copie par-dessus le travail de la commande. | |
| 82 | + | |
| 83 | +Quand une commande se termine, l'éditeur relit donc tous les fichiers ouverts. La partie intéressante est ceux qu'il refuse de toucher. | |
| 84 | + | |
| 85 | +**Un fichier avec des modifications non enregistrées est laissé tranquille**, et la barre d'état dit combien ont été ignorés. Le recharger jetterait un travail que l'utilisateur n'a pas enregistré, ce qu'aucune commodité ne justifie. Et le conflit est réel : la commande et la modification non enregistrée ne sont pas d'accord sur ce que le fichier doit dire, et l'éditeur n'est pas en position de trancher. Le nommer et s'arrêter est l'issue honnête. | |
| 86 | + | |
| 87 | +Deux décisions plus petites à l'intérieur : | |
| 88 | + | |
| 89 | +- **Le curseur reste où il était**, ramené dans ce que le fichier contient désormais. | |
| 90 | +- **L'historique d'annulation est jeté.** Annuler au-delà d'un rechargement restaurerait un texte que le fichier n'a plus, ce qui est pire que ne pas pouvoir annuler du tout. | |
| 91 | + | |
| 92 | +## Pourquoi le rechargement se fait sur la boucle d'événements | |
| 93 | + | |
| 94 | +La fin de la commande est remarquée sur la goroutine qui lit le terminal, laquelle n'a pas le droit de toucher un tampon ou le bureau. Elle pose donc un drapeau, et le rechargement s'exécute au début du tour suivant de la boucle d'événements. | |
| 95 | + | |
| 96 | +C'est la quatrième chose dans la bibliothèque construite ainsi — l'annonce du serveur de langage, les redessins du terminal, l'échéance de l'enregistrement automatique, et maintenant ceci. La règle qu'elles partagent vaut d'être redite : **le réveil peut se perdre, l'état ne doit pas se perdre.** `PostEvent` laisse tomber ce qui n'entre pas dans sa file, tout ce qui dépend de l'arrivée d'un message est donc un bogue qui attend un moment d'affluence. Un drapeau que la boucle vérifie elle-même ne peut pas manquer. | |
| 97 | + | |
| 98 | +## Pourquoi une commande peut demander une valeur, et pourquoi entre doubles accolades | |
| 99 | + | |
| 100 | +`golo` a besoin d'un script. `golo new` a besoin d'un nom de module et d'un nom de fichier. `gogolo build` a besoin d'un script et d'un chemin de sortie. `wagolo build` a besoin d'une cible en plus. Rien de cela ne peut vivre dans le fichier d'outils comme une chaîne fixe, parce que la réponse est différente chaque fois — et un outil qui ne peut pas demander est un outil qu'il faut éditer avant chaque usage, ce qui n'est pas un outil. | |
| 101 | + | |
| 102 | +Un `{{libellé}}` dans une commande est donc une valeur que l'éditeur demande d'abord, dans une boîte titrée du nom de l'outil. **Six des huit commandes Golo l'utilisent**, et c'est délibéré : une fonctionnalité démontrée dans le fichier que tout le monde reçoit est une fonctionnalité que les gens trouvent, et une décrite seulement dans un commentaire ne l'est pas. C'est plus de champs que dans le fichier de départ d'aucun frère, et la raison est celle de Golo : sans manifeste il n'y a pas de `moon run` qui sache quoi lancer, chaque commande qui touche un fichier doit donc se faire dire lequel. | |
| 103 | + | |
| 104 | +**Les accolades simples étaient l'orthographe évidente et elles sont fausses.** `awk '{print $1}'` et `find . -exec rm {} +` sont des choses ordinaires à mettre dans un fichier d'outils, et lire la première comme un champ transforme une commande qui marche en une boîte demandant « print $1 ». Les doubles accolades n'entrent en collision avec presque rien. | |
| 105 | + | |
| 106 | +**La valeur est citée par défaut**, parce que l'alternative échoue en silence. Un chemin avec un espace, substitué brut, devient deux arguments et la commande se plaint d'un fichier qui n'existe pas. Citer fait marcher ce cas et rend l'autre — « mets ces trois options à la fin » — impossible, si bien que `...` dans les accolades demande la valeur telle quelle. Deux comportements, tous deux documentés, plutôt qu'un seul qui a tort la moitié du temps. | |
| 107 | + | |
| 108 | +**Rien n'est mémorisé sur le disque.** La boîte part de ce qui a été tapé la fois précédente, pour la session. L'écrire dans le répertoire propre du projet a été envisagé et rejeté : ce répertoire contient ce que le projet a décidé, et le script que quelqu'un a lancé en traquant un bogue n'en fait pas partie. | |
| 109 | + | |
| 110 | +**Un fichier qui ne peut pas être parsé est refusé à la lecture**, pas quand l'outil est choisi. Un `{{` non fermé qui atteint le shell est une commande qui échoue avec des accolades dedans, ce qui ne nomme ni l'outil ni le fichier ; refuser au chargement nomme les deux. C'est la même règle que suit déjà une valeur d'`output` inconnue. | |
| 111 | + | |
| 112 | +**Le dialogue est refusé quand il ne tient pas.** Un outil qui demande plus de valeurs que le terminal n'a de lignes donnerait une boîte dont le bouton OK est sous le bas de l'écran — à laquelle on ne peut répondre que par Échap, qui annule. Dire « ceci demande douze valeurs et neuf tiennent » n'est pire que rien que si vous préférez le découvrir en essayant. | |
| 113 | + | |
| 114 | +## Liens avec le reste | |
| 115 | + | |
| 116 | +- Chaque clé du fichier et chaque règle : [Référence des outils Golo](../reference/golo-tools.md) | |
| 117 | +- S'en servir : [Lancer des commandes Golo depuis l'éditeur](../how-to/run-golo-commands.md) | |
| 118 | +- Les fenêtres qu'utilise `output = "terminal"`, et pourquoi ce sont de vrais terminaux : [Fenêtres de terminal](terminal-windows.md) | |
| 119 | +- L'autre menu construit à partir d'un fichier : [Snippets](snippets.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,119 @@ | |||
| 1 | +# Outils Golo — explication | ||
| 2 | + | ||
| 3 | +## De quoi s'agit-il ? | ||
| 4 | + | ||
| 5 | +Un menu **Golo** dont les commandes viennent d'un fichier TOML, chacune exécutée là où l'outil l'a demandé — un popup, une fenêtre de terminal ou une fenêtre d'édition — et les fichiers ouverts relus ensuite. Cette page explique pourquoi chacun de ces choix est ce qu'il est. | ||
| 6 | + | ||
| 7 | +## Pourquoi la sortie a trois destinations, et un popup par défaut | ||
| 8 | + | ||
| 9 | +La première version de ce mécanisme, dans Turbo Go, mettait chaque commande dans une fenêtre de terminal, et c'était le mauvais défaut pour la plupart d'entre elles. | ||
| 10 | + | ||
| 11 | +Un terminal est la bonne réponse quand le programme est *interactif ou long* : `golo main.golo` sur un script qui lit le clavier avec `readln` doit pouvoir recevoir une réponse, et un script qui sert du HTTP avec `httpServe` doit pouvoir être interrompu par `Ctrl-C`. Rien de cela n'est vrai de `golo --test`, qui imprime son rapport et se termine. Lui donner une fenêtre entière — qu'il faut ensuite fermer, sur un bureau où les fenêtres se chevauchent et sont numérotées — c'est plus de cérémonie que le résultat ne mérite. | ||
| 12 | + | ||
| 13 | +Un popup est la bonne réponse pour une commande qu'on lance, qu'on lit et qu'on écarte. Il est modal, ce qui est un vrai coût et est nommé dans le [guide](../how-to/run-golo-commands.md) : un `gogolo build` dont vous n'attendiez pas la lenteur — il lance le compilateur Go — retient l'éditeur jusqu'à ce qu'il finisse ou que vous appuyiez sur Échap. Ce coût a été accepté à dessein, parce que l'alternative — un dialogue qui surgit trois secondes plus tard — avale ce qu'on était en train de taper à l'instant où il arrive. | ||
| 14 | + | ||
| 15 | +Le popup **s'ouvre donc immédiatement et se remplit**. Vous voyez la progression, rien ne vous surprend, et Échap le ferme et arrête la commande, ce qui est la seule façon d'interrompre quelque chose dont la sortie n'est pas dans un terminal. | ||
| 16 | + | ||
| 17 | +Une fenêtre d'édition est la bonne réponse pour une sortie que vous allez parcourir : un long rapport de tests, ou le source Go que `gogolo transpile` imprime. C'est un tampon ordinaire, `Ctrl-F` le fouille et `Save as` le conserve. Elle est remplie une fois la commande terminée plutôt qu'au fil de l'eau, parce qu'un tampon qui grandit sous le curseur pendant qu'on le fouille est le contraire de ce pour quoi ce mode existe. | ||
| 18 | + | ||
| 19 | +Aucune des trois ne convient à tout, c'est pourquoi `output` est dans le fichier plutôt que dans le code. `Run` est l'exemple travaillé : c'est la première commande du fichier de départ, elle dit `terminal`, et le commentaire à côté dit pourquoi. `Debug` aussi — le débogueur pas à pas lit le clavier — et `REPL`, qui n'est rien d'autre qu'un clavier. | ||
| 20 | + | ||
| 21 | +## Pourquoi Run vient en premier | ||
| 22 | + | ||
| 23 | +Dans le fichier de départ de Turbo MoonBit le premier outil est `moon check`, parce que pour un langage compilé « est-ce correct ? » est la question posée le plus souvent et celle qui ne produit rien. Golo est un langage de script, et la question posée le plus souvent est « qu'est-ce que ça imprime ? ». La première ligne du menu Golo exécute donc le fichier, et la deuxième lance les tests. | ||
| 24 | + | ||
| 25 | +## Pourquoi une fenêtre de terminal est toujours là | ||
| 26 | + | ||
| 27 | +L'éditeur en avait déjà une — un vrai pseudo-terminal avec un émulateur VT, construit pour les fenêtres `F8` — si bien qu'`output = "terminal"` coûte un champ dans ses options et achète pour rien les couleurs, la pagination, `Ctrl-C`, la saisie clavier et le défilement arrière, parce que ce sont les mêmes mécanismes qu'utilise n'importe quel terminal. La sortie de GoloScript utilise ces couleurs : `golo --test` dessine des coches vertes, et la famille de builtins `uiPrint` dessine ce que le script a demandé. | ||
| 28 | + | ||
| 29 | +La fenêtre reste après la fin de la commande, et c'est le but : la sortie est ce que vous avez demandé, et une fenêtre qui disparaîtrait avec elle serait inutile. | ||
| 30 | + | ||
| 31 | +## Pourquoi le code de sortie est toujours dans le titre | ||
| 32 | + | ||
| 33 | +Un script qui se termine sans rien imprimer, ou `golo --test` sur un répertoire sans fichier de test, n'imprime rien du tout. Un popup au corps vide et au titre neutre ne se distingue pas d'un popup dont la commande n'a pas démarré, et le lecteur est laissé à deviner la seule chose qu'il voulait savoir. | ||
| 34 | + | ||
| 35 | +Le titre porte donc le verdict — `— ok` ou `— exit 1` — et un corps vide dit `(no output)` une fois la commande terminée. Pendant qu'elle tourne encore le corps reste vide, parce que « (no output) » est un verdict et qu'une commande en cours n'en a pas encore atteint. | ||
| 36 | + | ||
| 37 | +## Pourquoi les commandes sont dans un fichier | ||
| 38 | + | ||
| 39 | +Huit commandes câblées dans l'éditeur auraient répondu à la demande. Elles auraient aussi été fausses en une semaine. | ||
| 40 | + | ||
| 41 | +Chaque commande du fichier de départ passe par l'un des trois binaires de GoloScript — `golo`, `gogolo` ou `wagolo` — et aucune n'a donc besoin de quoi que ce soit au-delà de l'outillage lui-même. C'est un défaut défendable et ce n'est la réponse universelle de personne. Un projet à un seul point d'entrée veut `golo main.golo` sans qu'on lui demande quel script. Un projet livré en binaire natif veut `gogolo build -o bin/app app.golo` avec la sortie fixée. Un projet qui vise le navigateur veut `wagolo build -target=js` et jamais `wasi`. Un projet qui tourne sous Docker veut `docker run … k33g/gololang`. Rien de cela n'est connaissable d'ici, et tout cela est une ligne dans un fichier. | ||
| 42 | + | ||
| 43 | +Les huit sont donc **des défauts, pas du code** : c'est le contenu du fichier de départ que **Golo ▸ Create tools file** écrit, et en changer une revient à éditer un fichier plutôt qu'à reconstruire un éditeur. Le fichier est lu à chaque ouverture du menu, pour la même raison que le menu Snippets : une modification doit prendre effet aussitôt, et le fichier est souvent ouvert dans la fenêtre derrière le menu. | ||
| 44 | + | ||
| 45 | +Les commandes vont à `sh -c` — `cmd.exe /S /C` sous Windows — plutôt que d'être découpées en argv ici. Le fichier appartient à l'utilisateur, les tubes, les globs et `&&` sont donc des fonctionnalités plutôt que des dangers, et une entrée peut être `golo --test && gogolo build -o app main.golo`. Découper un argv reviendrait à inventer des règles de citation pour une chaîne que quelqu'un a écrite à la main. | ||
| 46 | + | ||
| 47 | +## Pourquoi il n'y a pas de fichier d'outils au niveau utilisateur | ||
| 48 | + | ||
| 49 | +Les snippets sont lus dans deux fichiers — le vôtre et celui du projet — parce que vos snippets sont vos habitudes et doivent vous suivre d'un projet à l'autre. | ||
| 50 | + | ||
| 51 | +Les outils ne sont pas ainsi. Ils appartiennent à l'outillage propre d'un projet : un fichier d'outils global proposerait `golo --test` dans un dépôt qui n'a jamais entendu parler de Golo, et un projet qui ne fait qu'interpréter ses scripts aurait `wagolo build` dans son menu avec un TinyGo qu'il n'a jamais installé. Le fichier est par projet, et c'est toute la règle. | ||
| 52 | + | ||
| 53 | +## Pourquoi un outil peut nommer son propre menu | ||
| 54 | + | ||
| 55 | +Un menu appelé **Golo** qui contient `docker compose up` est un mensonge sur ce qu'est le menu. Le premier fichier d'outils qu'on écrit dépasse Golo, parce que les commandes d'un projet ne portent pas toutes sur le langage dans lequel il est écrit : conteneurs, bases de données, déploiements, une cible de `Makefile` ajoutée en 2019. | ||
| 56 | + | ||
| 57 | +Deux formes ont été envisagées. Un **second menu fixe** appelé Tools — tout ce qui est Golo dans Golo, tout le reste dans Tools — c'est une clé dans le format et aucun problème de nommage, mais cela ne fait que déplacer le mensonge : un menu Tools contenant `docker compose up`, `psql` et un script de déploiement est tout aussi indifférencié, et dès qu'il y a dix entrées personne n'en retrouve une. Et un **second fichier**, `menus.toml`, garde le fichier d'outils simple au prix de deux fichiers qui doivent s'accorder sur les outils existants. | ||
| 58 | + | ||
| 59 | +Le menu est donc un **nom libre sur l'outil**, dans le seul fichier : `menu = "Docker"`. Un nom que rien d'autre n'utilise crée le menu ; omettre la clé signifie Golo. Il n'y a pas de liste de noms autorisés, parce qu'une liste serait la liste des projets de quelqu'un d'autre. | ||
| 60 | + | ||
| 61 | +Golo lui-même reste fixe sur la barre plutôt que de devenir un nom de plus venu du fichier. **Golo ▸ Create tools file** doit être atteignable dans un projet qui n'a aucun fichier d'outils — c'est exactement le projet qui en a besoin — et un menu qui n'existe qu'une fois le fichier là ne peut pas proposer de l'écrire. | ||
| 62 | + | ||
| 63 | +## Pourquoi la touche chaude n'est pas au fichier de la choisir | ||
| 64 | + | ||
| 65 | +L'auteur d'un fichier d'outils ne peut pas savoir quelles lettres sont libres. Il voit `File`, `Edit`, `Search`, `Run`, `Code`, `Options`, `Window`, `Snippets`, `Golo` et `Help` sur la barre, mais seulement en comptant les soulignés, et un projet partagé entre plusieurs personnes dépendrait alors de ce que personne n'ajoute un menu qui entre en collision. | ||
| 66 | + | ||
| 67 | +Les collisions ici sont **silencieuses**, et c'est ce qui rend la conception nécessaire. La barre répond au premier menu dont la touche correspond ; un second menu revendiquant la même lettre n'est pas une erreur et se dessine normalement — il ne s'ouvre simplement jamais. Ce piège s'est déjà refermé une fois dans cette famille : `Snippets` et `Search` voulaient tous deux `S`, `Snippets` était l'inatteignable, et tous les tests passaient. La correction fut alors de déplacer Snippets sur `N` à la main. Laisser un fichier nommer des menus en fait un danger permanent plutôt qu'une erreur ponctuelle, l'attribution est donc faite par l'éditeur : la première lettre du nom que rien d'autre ne revendique. | ||
| 68 | + | ||
| 69 | +Les tildes écrits dans le nom sont honorés **quand la lettre est libre**, et remplacés sans bruit quand elle ne l'est pas. Refuser le fichier était l'alternative, et c'est pire : la collision dépend des menus existants, un fichier d'outils qui marchait casserait donc le jour où une version de l'éditeur ajoute un menu. Entre un menu sur une lettre que vous n'avez pas demandée et un menu que vous ne pouvez pas ouvrir, le premier est la moindre perte. | ||
| 70 | + | ||
| 71 | +Quand toutes les lettres d'un nom sont prises, le menu n'a aucune touche chaude. `F10`, les flèches et la souris l'atteignent toujours, et l'alternative — prendre une lettre qui n'est pas dans le nom — mettrait un souligné sous rien. | ||
| 72 | + | ||
| 73 | +## Pourquoi la barre est reconstruite à partir d'un stat | ||
| 74 | + | ||
| 75 | +`Menu.OnOpen` regarnit les entrées d'un menu juste avant qu'il se déroule, et c'est ainsi que les menus Golo et Snippets suivent leurs fichiers sans redémarrage. Cela ne peut pas aider ici : l'*ensemble* des menus fait partie de la barre, pas d'un menu en particulier, et ajouter `menu = "Docker"` au fichier doit mettre Docker sur la barre. | ||
| 76 | + | ||
| 77 | +Lire et parser le fichier à chaque tour de la boucle d'événements le ferait, et ce serait aussi du travail pour rien à chaque frappe dans un fichier que personne n'a modifié. La barre porte donc la taille et la date de modification du fichier d'outils à partir duquel elle a été construite, et un `stat` par tour décide s'il faut la reconstruire. Éditer le fichier dans la fenêtre devant soi, l'enregistrer, et voir la barre changer, c'est le cas pour lequel c'est fait. | ||
| 78 | + | ||
| 79 | +## Pourquoi les fichiers ouverts sont relus, et seulement certains | ||
| 80 | + | ||
| 81 | +Golo n'a pas de formateur, aucune commande de départ ne réécrit donc le fichier devant vous — mais `golo new` écrit un fichier dans le répertoire, `gogolo build -keep-go` laisse un `.go` à côté du script, et vos propres outils peuvent faire n'importe quoi. Sans rien de plus, l'éditeur resterait sur une copie périmée d'un fichier qu'une autre commande a changé, et le `F2` suivant écrirait votre copie par-dessus le travail de la commande. | ||
| 82 | + | ||
| 83 | +Quand une commande se termine, l'éditeur relit donc tous les fichiers ouverts. La partie intéressante est ceux qu'il refuse de toucher. | ||
| 84 | + | ||
| 85 | +**Un fichier avec des modifications non enregistrées est laissé tranquille**, et la barre d'état dit combien ont été ignorés. Le recharger jetterait un travail que l'utilisateur n'a pas enregistré, ce qu'aucune commodité ne justifie. Et le conflit est réel : la commande et la modification non enregistrée ne sont pas d'accord sur ce que le fichier doit dire, et l'éditeur n'est pas en position de trancher. Le nommer et s'arrêter est l'issue honnête. | ||
| 86 | + | ||
| 87 | +Deux décisions plus petites à l'intérieur : | ||
| 88 | + | ||
| 89 | +- **Le curseur reste où il était**, ramené dans ce que le fichier contient désormais. | ||
| 90 | +- **L'historique d'annulation est jeté.** Annuler au-delà d'un rechargement restaurerait un texte que le fichier n'a plus, ce qui est pire que ne pas pouvoir annuler du tout. | ||
| 91 | + | ||
| 92 | +## Pourquoi le rechargement se fait sur la boucle d'événements | ||
| 93 | + | ||
| 94 | +La fin de la commande est remarquée sur la goroutine qui lit le terminal, laquelle n'a pas le droit de toucher un tampon ou le bureau. Elle pose donc un drapeau, et le rechargement s'exécute au début du tour suivant de la boucle d'événements. | ||
| 95 | + | ||
| 96 | +C'est la quatrième chose dans la bibliothèque construite ainsi — l'annonce du serveur de langage, les redessins du terminal, l'échéance de l'enregistrement automatique, et maintenant ceci. La règle qu'elles partagent vaut d'être redite : **le réveil peut se perdre, l'état ne doit pas se perdre.** `PostEvent` laisse tomber ce qui n'entre pas dans sa file, tout ce qui dépend de l'arrivée d'un message est donc un bogue qui attend un moment d'affluence. Un drapeau que la boucle vérifie elle-même ne peut pas manquer. | ||
| 97 | + | ||
| 98 | +## Pourquoi une commande peut demander une valeur, et pourquoi entre doubles accolades | ||
| 99 | + | ||
| 100 | +`golo` a besoin d'un script. `golo new` a besoin d'un nom de module et d'un nom de fichier. `gogolo build` a besoin d'un script et d'un chemin de sortie. `wagolo build` a besoin d'une cible en plus. Rien de cela ne peut vivre dans le fichier d'outils comme une chaîne fixe, parce que la réponse est différente chaque fois — et un outil qui ne peut pas demander est un outil qu'il faut éditer avant chaque usage, ce qui n'est pas un outil. | ||
| 101 | + | ||
| 102 | +Un `{{libellé}}` dans une commande est donc une valeur que l'éditeur demande d'abord, dans une boîte titrée du nom de l'outil. **Six des huit commandes Golo l'utilisent**, et c'est délibéré : une fonctionnalité démontrée dans le fichier que tout le monde reçoit est une fonctionnalité que les gens trouvent, et une décrite seulement dans un commentaire ne l'est pas. C'est plus de champs que dans le fichier de départ d'aucun frère, et la raison est celle de Golo : sans manifeste il n'y a pas de `moon run` qui sache quoi lancer, chaque commande qui touche un fichier doit donc se faire dire lequel. | ||
| 103 | + | ||
| 104 | +**Les accolades simples étaient l'orthographe évidente et elles sont fausses.** `awk '{print $1}'` et `find . -exec rm {} +` sont des choses ordinaires à mettre dans un fichier d'outils, et lire la première comme un champ transforme une commande qui marche en une boîte demandant « print $1 ». Les doubles accolades n'entrent en collision avec presque rien. | ||
| 105 | + | ||
| 106 | +**La valeur est citée par défaut**, parce que l'alternative échoue en silence. Un chemin avec un espace, substitué brut, devient deux arguments et la commande se plaint d'un fichier qui n'existe pas. Citer fait marcher ce cas et rend l'autre — « mets ces trois options à la fin » — impossible, si bien que `...` dans les accolades demande la valeur telle quelle. Deux comportements, tous deux documentés, plutôt qu'un seul qui a tort la moitié du temps. | ||
| 107 | + | ||
| 108 | +**Rien n'est mémorisé sur le disque.** La boîte part de ce qui a été tapé la fois précédente, pour la session. L'écrire dans le répertoire propre du projet a été envisagé et rejeté : ce répertoire contient ce que le projet a décidé, et le script que quelqu'un a lancé en traquant un bogue n'en fait pas partie. | ||
| 109 | + | ||
| 110 | +**Un fichier qui ne peut pas être parsé est refusé à la lecture**, pas quand l'outil est choisi. Un `{{` non fermé qui atteint le shell est une commande qui échoue avec des accolades dedans, ce qui ne nomme ni l'outil ni le fichier ; refuser au chargement nomme les deux. C'est la même règle que suit déjà une valeur d'`output` inconnue. | ||
| 111 | + | ||
| 112 | +**Le dialogue est refusé quand il ne tient pas.** Un outil qui demande plus de valeurs que le terminal n'a de lignes donnerait une boîte dont le bouton OK est sous le bas de l'écran — à laquelle on ne peut répondre que par Échap, qui annule. Dire « ceci demande douze valeurs et neuf tiennent » n'est pire que rien que si vous préférez le découvrir en essayant. | ||
| 113 | + | ||
| 114 | +## Liens avec le reste | ||
| 115 | + | ||
| 116 | +- Chaque clé du fichier et chaque règle : [Référence des outils Golo](../reference/golo-tools.md) | ||
| 117 | +- S'en servir : [Lancer des commandes Golo depuis l'éditeur](../how-to/run-golo-commands.md) | ||
| 118 | +- Les fenêtres qu'utilise `output = "terminal"`, et pourquoi ce sont de vrais terminaux : [Fenêtres de terminal](terminal-windows.md) | ||
| 119 | +- L'autre menu construit à partir d'un fichier : [Snippets](snippets.md) | ||
added
docs/fr/explanation/project-settings.md +68 -0 | new file mode 100644 | ||
| @@ -0,0 +1,68 @@ | ||
| 1 | +# Réglages de projet — explication | |
| 2 | + | |
| 3 | +## De quoi s'agit-il ? | |
| 4 | + | |
| 5 | +Un projet peut garder un `.turbo-golo/settings.toml` à côté de son code, disant quel thème utiliser et s'il faut enregistrer les fichiers automatiquement. Cette page traite des décisions contenues dans cette phrase : pourquoi le fichier n'est cherché qu'à un seul endroit, pourquoi le créer est une entrée de menu plutôt que quelque chose qui arrive tout seul, et pourquoi la sauvegarde automatique fonctionne comme elle le fait. | |
| 6 | + | |
| 7 | +## Pourquoi le dossier n'est pas cherché vers le haut | |
| 8 | + | |
| 9 | +Dans les éditeurs Turbo dont le langage a un manifeste, le serveur de langage remonte depuis le fichier ouvert jusqu'à le croiser, parce qu'un module a une frontière réelle : le fichier est au-dessus de vous ou il n'y est pas, et être dans un module est un fait à propos du code. Golo n'a pas de manifeste, et Turbo Golo ne remonte donc vers rien — le serveur reçoit simplement le dossier du fichier ouvert. Le fichier de réglages, lui, n'est cherché que dans le répertoire de travail, et ce serait le cas même si Golo avait un manifeste. | |
| 10 | + | |
| 11 | +« Le projet » n'est pas un fait à propos du code. C'est l'endroit où vous avez décidé de travailler, et la même arborescence est plusieurs projets selon ce que vous y faites — le `services/api` d'un monorepo est un projet quand vous travaillez sur l'API, et une partie d'un plus grand le reste du temps. | |
| 12 | + | |
| 13 | +Une remontée ferait aussi agir le réglage à distance. Vous ouvrez un fichier, et les couleurs de l'éditeur changent à cause d'un fichier trois dossiers plus haut dont vous ignoriez l'existence. Toute explication de ce comportement commence par « eh bien, il cherche vers le haut », alors que la règle qu'on préfère pouvoir énoncer est celle qui est maintenant vraie : **le projet est le dossier depuis lequel vous avez lancé l'éditeur.** | |
| 14 | + | |
| 15 | +Le coût est réel et mérite d'être nommé. Lancez l'éditeur depuis un sous-dossier et le thème du projet ne s'applique pas. La réponse est de lancer depuis la racine du projet, là où vous feriez `golo --test` et `git` de toute façon. | |
| 16 | + | |
| 17 | +## Pourquoi créer le fichier est une entrée de menu | |
| 18 | + | |
| 19 | +L'autre solution était tentante : à la première fois qu'on choisit un thème, écrire `.turbo-golo/settings.toml` pour que le choix persiste. Tout éditeur qui stocke un état d'espace de travail fait quelque chose d'approchant. | |
| 20 | + | |
| 21 | +Elle a été écartée parce qu'elle dépose un dossier dans le dépôt de quelqu'un comme effet de bord d'un essai de couleur. L'utilisateur est à un `git status` d'une modification qu'il n'a pas faite, dans un projet qui n'est peut-être pas le sien, éventuellement en pleine revue. Un thème choisi pour être regardé dix secondes ne doit rien laisser derrière lui. | |
| 22 | + | |
| 23 | +Le fichier n'est donc créé que par **Options ▸ Create project settings**, et son existence signifie quelque chose : ce projet a des réglages, exprès. C'est aussi ce qui rend la règle d'écriture simple à énoncer — **le thème est écrit dans le fichier quand le fichier existe, et pas autrement** — sans nulle part une case « voulez-vous vous en souvenir ? ». | |
| 24 | + | |
| 25 | +## Pourquoi le thème est réécrit sur place plutôt que réencodé | |
| 26 | + | |
| 27 | +Une fois le fichier créé, choisir un thème le réécrit. Sérialiser la structure `Settings` vers du TOML ferait quatre lignes et supprimerait tous les commentaires du fichier. | |
| 28 | + | |
| 29 | +Cela compte plus ici qu'ailleurs, parce que ce fichier est *fait* pour être édité à la main. C'est la raison d'être de la coloration TOML dans l'éditeur ; le fichier créé est surtout des commentaires expliquant les clés ; une équipe y ajoutera ses propres commentaires disant pourquoi elle a choisi ce qu'elle a choisi. Tout perdre à la première tentative d'un autre thème serait une suppression silencieuse et surprenante de ce que quelqu'un a écrit. | |
| 30 | + | |
| 31 | +La réécriture trouve donc la ligne `theme` dans la table `[editor]` et change la valeur entre le `=` et un éventuel commentaire de fin de ligne. Tout le reste du fichier revient octet pour octet. Cela fait une quarantaine de lignes au lieu de quatre, et c'est la différence entre un fichier où l'on peut mettre des choses et un fichier qui les mange. | |
| 32 | + | |
| 33 | +## Pourquoi la sauvegarde automatique attend une pause | |
| 34 | + | |
| 35 | +Trois déclencheurs ont été envisagés. | |
| 36 | + | |
| 37 | +**À intervalle fixe** est le plus simple et il est faux : il écrit au milieu d'une modification. La moitié d'un identifiant renommé atteint le disque, un observateur de fichiers relance les tests, et une suite échoue sur du code qui n'a jamais été l'intention de personne. | |
| 38 | + | |
| 39 | +**À la sortie de la fenêtre** n'écrit jamais pendant qu'on travaille, ce qui semble prudent et signifie que ce qui est sur le disque peut avoir une heure de retard sur ce qui est à l'écran — précisément quand cela compte, puisque la raison de vouloir l'autosave est en général un outil qui surveille le fichier. | |
| 40 | + | |
| 41 | +**Après une pause dans la frappe** est ce sur quoi les autres éditeurs et celui-ci se sont arrêtés. Deux secondes, c'est assez long pour qu'une pause de réflexion ne soit pas une écriture, assez court pour qu'un `golo --test` relancé suive de près une modification. Une série de frappes fait une écriture, pas une par touche. | |
| 42 | + | |
| 43 | +Il y a une seule échéance pour tout l'éditeur plutôt qu'une par fenêtre, parce que « vous avez cessé de taper » est un seul événement. Une échéance par fenêtre enregistrerait le fichier que vous avez quitté à un moment différent de celui que vous avez sous les yeux, ce que personne ne pourrait observer et qui fait davantage d'état à maintenir juste. | |
| 44 | + | |
| 45 | +## Pourquoi l'échéance est vérifiée, le minuteur ne faisant que pousser | |
| 46 | + | |
| 47 | +C'est le même piège que l'annonce au serveur de langage et que les redessins de terminal ont rencontré, et il vaut d'être énoncé une fois de plus parce qu'il reviendra. | |
| 48 | + | |
| 49 | +L'éditeur est bloqué dans `PollEvent`. Pour remarquer une échéance alors que rien ne se passe, il faut le réveiller, et le seul moyen de le réveiller depuis un minuteur est `PostEvent` — qui **jette** les événements quand sa file est pleine. | |
| 50 | + | |
| 51 | +Le minuteur n'est donc pas ce qui décide. L'échéance est un état, vérifié en tête de chaque tour de boucle, exactement comme `announceOpenDocuments` vérifie si le serveur de langage est prêt. Le seul rôle du minuteur est de garantir qu'un tour ait lieu. Une poussée perdue coûte une sauvegarde en retard jusqu'à la frappe ou au clic suivant ; un design où le minuteur enregistrerait lui-même la perdrait tout court. | |
| 52 | + | |
| 53 | +## Pourquoi une sauvegarde automatique en échec n'ouvre pas de dialogue | |
| 54 | + | |
| 55 | +Une sauvegarde que personne n'a demandée ne doit pas interrompre par une fenêtre modale, et une modale qui revient toutes les deux secondes parce qu'un fichier est en lecture seule est pire que le problème qu'elle signale. Cela va donc dans la barre d'état, et l'échéance est effacée *avant* la tentative d'écriture : un fichier qui ne peut pas être écrit est essayé une fois par modification, et non indéfiniment. | |
| 56 | + | |
| 57 | +## Pourquoi la coloration TOML réutilise les classes existantes | |
| 58 | + | |
| 59 | +Ajouter `syntax.tomlkey` et compagnie aurait signifié que tous les thèmes — y compris ceux écrits par les utilisateurs — auraient silencieusement échoué à colorer le TOML jusqu'à leur mise à jour. | |
| 60 | + | |
| 61 | +Les classes déjà présentes conviennent : un en-tête de table nomme une structure, il se lit donc comme un type ; une clé nomme une chose, elle se lit donc comme un identifiant ; `true` et `false` sont des constantes parce que c'est ce qu'elles sont. Le résultat est que tout thème qui a jamais fonctionné colore le TOML correctement, sans modification et sans nouvelle clé. Le scanner est écrit à la main pour la même raison que l'émulateur de terminal — le TOML est un langage petit et entièrement spécifié, et c'est un fichier contre une troisième dépendance. | |
| 62 | + | |
| 63 | +## Liens avec le reste | |
| 64 | + | |
| 65 | +- Les clés exactes et leurs valeurs par défaut : [Référence des réglages de projet](../reference/project-settings.md) | |
| 66 | +- En mettre en place : [Donner ses propres réglages à un projet](../how-to/configure-a-project.md) | |
| 67 | +- L'autre fichier TOML que lit l'éditeur : [Format des fichiers de thème](../reference/themes.md) | |
| 68 | +- Où `settings` se situe parmi les paquets : [Architecture](architecture.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,68 @@ | |||
| 1 | +# Réglages de projet — explication | ||
| 2 | + | ||
| 3 | +## De quoi s'agit-il ? | ||
| 4 | + | ||
| 5 | +Un projet peut garder un `.turbo-golo/settings.toml` à côté de son code, disant quel thème utiliser et s'il faut enregistrer les fichiers automatiquement. Cette page traite des décisions contenues dans cette phrase : pourquoi le fichier n'est cherché qu'à un seul endroit, pourquoi le créer est une entrée de menu plutôt que quelque chose qui arrive tout seul, et pourquoi la sauvegarde automatique fonctionne comme elle le fait. | ||
| 6 | + | ||
| 7 | +## Pourquoi le dossier n'est pas cherché vers le haut | ||
| 8 | + | ||
| 9 | +Dans les éditeurs Turbo dont le langage a un manifeste, le serveur de langage remonte depuis le fichier ouvert jusqu'à le croiser, parce qu'un module a une frontière réelle : le fichier est au-dessus de vous ou il n'y est pas, et être dans un module est un fait à propos du code. Golo n'a pas de manifeste, et Turbo Golo ne remonte donc vers rien — le serveur reçoit simplement le dossier du fichier ouvert. Le fichier de réglages, lui, n'est cherché que dans le répertoire de travail, et ce serait le cas même si Golo avait un manifeste. | ||
| 10 | + | ||
| 11 | +« Le projet » n'est pas un fait à propos du code. C'est l'endroit où vous avez décidé de travailler, et la même arborescence est plusieurs projets selon ce que vous y faites — le `services/api` d'un monorepo est un projet quand vous travaillez sur l'API, et une partie d'un plus grand le reste du temps. | ||
| 12 | + | ||
| 13 | +Une remontée ferait aussi agir le réglage à distance. Vous ouvrez un fichier, et les couleurs de l'éditeur changent à cause d'un fichier trois dossiers plus haut dont vous ignoriez l'existence. Toute explication de ce comportement commence par « eh bien, il cherche vers le haut », alors que la règle qu'on préfère pouvoir énoncer est celle qui est maintenant vraie : **le projet est le dossier depuis lequel vous avez lancé l'éditeur.** | ||
| 14 | + | ||
| 15 | +Le coût est réel et mérite d'être nommé. Lancez l'éditeur depuis un sous-dossier et le thème du projet ne s'applique pas. La réponse est de lancer depuis la racine du projet, là où vous feriez `golo --test` et `git` de toute façon. | ||
| 16 | + | ||
| 17 | +## Pourquoi créer le fichier est une entrée de menu | ||
| 18 | + | ||
| 19 | +L'autre solution était tentante : à la première fois qu'on choisit un thème, écrire `.turbo-golo/settings.toml` pour que le choix persiste. Tout éditeur qui stocke un état d'espace de travail fait quelque chose d'approchant. | ||
| 20 | + | ||
| 21 | +Elle a été écartée parce qu'elle dépose un dossier dans le dépôt de quelqu'un comme effet de bord d'un essai de couleur. L'utilisateur est à un `git status` d'une modification qu'il n'a pas faite, dans un projet qui n'est peut-être pas le sien, éventuellement en pleine revue. Un thème choisi pour être regardé dix secondes ne doit rien laisser derrière lui. | ||
| 22 | + | ||
| 23 | +Le fichier n'est donc créé que par **Options ▸ Create project settings**, et son existence signifie quelque chose : ce projet a des réglages, exprès. C'est aussi ce qui rend la règle d'écriture simple à énoncer — **le thème est écrit dans le fichier quand le fichier existe, et pas autrement** — sans nulle part une case « voulez-vous vous en souvenir ? ». | ||
| 24 | + | ||
| 25 | +## Pourquoi le thème est réécrit sur place plutôt que réencodé | ||
| 26 | + | ||
| 27 | +Une fois le fichier créé, choisir un thème le réécrit. Sérialiser la structure `Settings` vers du TOML ferait quatre lignes et supprimerait tous les commentaires du fichier. | ||
| 28 | + | ||
| 29 | +Cela compte plus ici qu'ailleurs, parce que ce fichier est *fait* pour être édité à la main. C'est la raison d'être de la coloration TOML dans l'éditeur ; le fichier créé est surtout des commentaires expliquant les clés ; une équipe y ajoutera ses propres commentaires disant pourquoi elle a choisi ce qu'elle a choisi. Tout perdre à la première tentative d'un autre thème serait une suppression silencieuse et surprenante de ce que quelqu'un a écrit. | ||
| 30 | + | ||
| 31 | +La réécriture trouve donc la ligne `theme` dans la table `[editor]` et change la valeur entre le `=` et un éventuel commentaire de fin de ligne. Tout le reste du fichier revient octet pour octet. Cela fait une quarantaine de lignes au lieu de quatre, et c'est la différence entre un fichier où l'on peut mettre des choses et un fichier qui les mange. | ||
| 32 | + | ||
| 33 | +## Pourquoi la sauvegarde automatique attend une pause | ||
| 34 | + | ||
| 35 | +Trois déclencheurs ont été envisagés. | ||
| 36 | + | ||
| 37 | +**À intervalle fixe** est le plus simple et il est faux : il écrit au milieu d'une modification. La moitié d'un identifiant renommé atteint le disque, un observateur de fichiers relance les tests, et une suite échoue sur du code qui n'a jamais été l'intention de personne. | ||
| 38 | + | ||
| 39 | +**À la sortie de la fenêtre** n'écrit jamais pendant qu'on travaille, ce qui semble prudent et signifie que ce qui est sur le disque peut avoir une heure de retard sur ce qui est à l'écran — précisément quand cela compte, puisque la raison de vouloir l'autosave est en général un outil qui surveille le fichier. | ||
| 40 | + | ||
| 41 | +**Après une pause dans la frappe** est ce sur quoi les autres éditeurs et celui-ci se sont arrêtés. Deux secondes, c'est assez long pour qu'une pause de réflexion ne soit pas une écriture, assez court pour qu'un `golo --test` relancé suive de près une modification. Une série de frappes fait une écriture, pas une par touche. | ||
| 42 | + | ||
| 43 | +Il y a une seule échéance pour tout l'éditeur plutôt qu'une par fenêtre, parce que « vous avez cessé de taper » est un seul événement. Une échéance par fenêtre enregistrerait le fichier que vous avez quitté à un moment différent de celui que vous avez sous les yeux, ce que personne ne pourrait observer et qui fait davantage d'état à maintenir juste. | ||
| 44 | + | ||
| 45 | +## Pourquoi l'échéance est vérifiée, le minuteur ne faisant que pousser | ||
| 46 | + | ||
| 47 | +C'est le même piège que l'annonce au serveur de langage et que les redessins de terminal ont rencontré, et il vaut d'être énoncé une fois de plus parce qu'il reviendra. | ||
| 48 | + | ||
| 49 | +L'éditeur est bloqué dans `PollEvent`. Pour remarquer une échéance alors que rien ne se passe, il faut le réveiller, et le seul moyen de le réveiller depuis un minuteur est `PostEvent` — qui **jette** les événements quand sa file est pleine. | ||
| 50 | + | ||
| 51 | +Le minuteur n'est donc pas ce qui décide. L'échéance est un état, vérifié en tête de chaque tour de boucle, exactement comme `announceOpenDocuments` vérifie si le serveur de langage est prêt. Le seul rôle du minuteur est de garantir qu'un tour ait lieu. Une poussée perdue coûte une sauvegarde en retard jusqu'à la frappe ou au clic suivant ; un design où le minuteur enregistrerait lui-même la perdrait tout court. | ||
| 52 | + | ||
| 53 | +## Pourquoi une sauvegarde automatique en échec n'ouvre pas de dialogue | ||
| 54 | + | ||
| 55 | +Une sauvegarde que personne n'a demandée ne doit pas interrompre par une fenêtre modale, et une modale qui revient toutes les deux secondes parce qu'un fichier est en lecture seule est pire que le problème qu'elle signale. Cela va donc dans la barre d'état, et l'échéance est effacée *avant* la tentative d'écriture : un fichier qui ne peut pas être écrit est essayé une fois par modification, et non indéfiniment. | ||
| 56 | + | ||
| 57 | +## Pourquoi la coloration TOML réutilise les classes existantes | ||
| 58 | + | ||
| 59 | +Ajouter `syntax.tomlkey` et compagnie aurait signifié que tous les thèmes — y compris ceux écrits par les utilisateurs — auraient silencieusement échoué à colorer le TOML jusqu'à leur mise à jour. | ||
| 60 | + | ||
| 61 | +Les classes déjà présentes conviennent : un en-tête de table nomme une structure, il se lit donc comme un type ; une clé nomme une chose, elle se lit donc comme un identifiant ; `true` et `false` sont des constantes parce que c'est ce qu'elles sont. Le résultat est que tout thème qui a jamais fonctionné colore le TOML correctement, sans modification et sans nouvelle clé. Le scanner est écrit à la main pour la même raison que l'émulateur de terminal — le TOML est un langage petit et entièrement spécifié, et c'est un fichier contre une troisième dépendance. | ||
| 62 | + | ||
| 63 | +## Liens avec le reste | ||
| 64 | + | ||
| 65 | +- Les clés exactes et leurs valeurs par défaut : [Référence des réglages de projet](../reference/project-settings.md) | ||
| 66 | +- En mettre en place : [Donner ses propres réglages à un projet](../how-to/configure-a-project.md) | ||
| 67 | +- L'autre fichier TOML que lit l'éditeur : [Format des fichiers de thème](../reference/themes.md) | ||
| 68 | +- Où `settings` se situe parmi les paquets : [Architecture](architecture.md) | ||
added
docs/fr/explanation/project-tree.md +58 -0 | new file mode 100644 | ||
| @@ -0,0 +1,58 @@ | ||
| 1 | +# Arbre du projet — explication | |
| 2 | + | |
| 3 | +## De quoi s'agit-il ? | |
| 4 | + | |
| 5 | +`F9` ouvre une fenêtre listant les fichiers du projet, et `Entrée` sur l'un d'eux l'ouvre. Cette page traite des trois décisions contenues là-dedans : où l'arbre s'enracine, pourquoi c'est une fenêtre plutôt qu'un panneau sur le côté, et pourquoi il ne remarque pas tout seul l'apparition de fichiers. | |
| 6 | + | |
| 7 | +## Pourquoi la racine est le répertoire de travail | |
| 8 | + | |
| 9 | +L'éditeur contient déjà deux réponses différentes à « qu'est-ce que le projet ». | |
| 10 | + | |
| 11 | +Le serveur de langage reçoit le dossier du fichier ouvert, parce que c'est tout ce dont `golo lsp` a besoin : Golo n'a pas de manifeste de projet, et le serveur répond sur le fichier qu'on lui donne. Le fichier de réglages, lui, ne regarde qu'un seul endroit : `.turbo-golo/settings.toml` est cherché dans le répertoire de travail et nulle part ailleurs. | |
| 12 | + | |
| 13 | +L'arbre suit le fichier de réglages, et il vaut la peine de dire pourquoi l'*autre* règle était tentante. Enraciner l'arbre sur le dossier du fichier ouvert — ou remonter jusqu'à un `.git` — ferait qu'ouvrir un fichier de n'importe où dans un projet montrerait tout le projet, ce que fait habituellement un explorateur. Mais cela signifie aussi que la racine de l'arbre dépend d'un dossier auquel vous n'avez peut-être pas pensé, et cela cesse d'être prévisible dès qu'un dépôt contient plusieurs programmes — un monorepo vous montrerait le dossier auquel appartient le fichier que vous avez ouvert par hasard. | |
| 14 | + | |
| 15 | +La règle retenue est celle qui tient en une phrase et qui est vraie partout dans l'éditeur : **le projet est le dossier depuis lequel vous avez lancé l'éditeur.** Elle coûte quelque chose, et ce coût est nommé dans le [guide](../how-to/browse-a-project.md) : lancez depuis un sous-dossier et vous obtenez l'arbre de ce sous-dossier. La réponse est de lancer depuis la racine du projet, là où vous feriez `golo --test` et `git` de toute façon. | |
| 16 | + | |
| 17 | +## Pourquoi `.git` est masqué et rien d'autre | |
| 18 | + | |
| 19 | +Le dialogue Open masque toute entrée commençant par un point. Recopier cela ici était la chose évidente, et aurait été faux. | |
| 20 | + | |
| 21 | +`.turbo-golo/settings.toml` est un fichier que cet éditeur demande aux gens d'éditer — c'est la raison d'être de la coloration TOML. `.gitignore` et `.qlty/qlty.toml` sont eux aussi des fichiers du projet. Un arbre qui les cacherait rendrait la configuration de l'éditeur inaccessible depuis l'explorateur de fichiers de l'éditeur, ce qui est un endroit curieux où aboutir. | |
| 22 | + | |
| 23 | +`.git` diffère par nature et non par orthographe : rien à l'intérieur n'est fait pour être ouvert à la main, et il contient assez d'objets pour enterrer tout le reste dans la liste. Un seul nom, masqué pour une raison énonçable. Respecter aussi `.gitignore` a été envisagé et écarté pour l'instant : cela masquerait `bin/` et `release/`, ce qui serait réellement plus agréable, et cela coûte un moteur de motifs gitignore — négations, `**`, ancrage — qui est une fonctionnalité à part entière plutôt qu'un détail d'arbre. | |
| 24 | + | |
| 25 | +## Pourquoi une fenêtre, pas un panneau | |
| 26 | + | |
| 27 | +Tous les autres éditeurs mettent leur arbre de fichiers dans une bande fixe à gauche. C'était l'alternative, et elle a été écartée à cause de ce qu'elle aurait coûté au reste de l'éditeur. | |
| 28 | + | |
| 29 | +Un panneau ancré signifie que le bureau n'est plus un simple rectangle où vivent les fenêtres. `Desktop` aurait besoin d'une notion de bords réservés ; `Window.fitInto` et les modes de croissance devraient les respecter ; agrandir voudrait dire « tout le bureau sauf le panneau » ; la mise en mosaïque et en cascade devraient en tenir compte. C'est une modification des fondations de toute l'interface, pour un seul widget. | |
| 30 | + | |
| 31 | +En fenêtre ordinaire, l'arbre obtient tout gratuitement et se comporte comme le reste : `F6` l'atteint, `Alt-2` le remonte, `[x]` le ferme, `[■]` lui donne tout le bureau, **Window ▸ Tile** le met à côté de votre fichier. Rien dans `ui` n'a eu à changer. Si un panneau ancré est voulu plus tard, c'est une fonctionnalité de `ui` à concevoir pour elle-même, plutôt qu'à faire passer en douce avec un explorateur de fichiers. | |
| 32 | + | |
| 33 | +## Pourquoi il n'y en a qu'un | |
| 34 | + | |
| 35 | +Deux arbres sur le même projet seraient deux vues d'une même chose sans rien pour les distinguer, et le projet ne peut pas changer pendant que l'éditeur tourne — la racine est fixée au démarrage. `F9` sur un arbre ouvert le remonte donc au lieu d'en créer un autre, exactement comme ouvrir un fichier déjà ouvert remonte sa fenêtre. | |
| 36 | + | |
| 37 | +## Pourquoi il ne surveille pas le disque | |
| 38 | + | |
| 39 | +Un arbre qui remarquerait `gogolo build` produisant un exécutable serait meilleur. Le faire correctement signifie surveiller le système de fichiers, et en Go cela signifie `fsnotify` — une troisième dépendance, contre une bibliothèque qui s'en tient à deux depuis le début et qui traite l'ajout d'une dépendance comme une décision à défendre. | |
| 40 | + | |
| 41 | +Ce n'est pas non plus une petite dépendance en comportement : surveillances récursives, descripteurs épuisés sur les grosses arborescences, et une sémantique différente sur chaque plateforme — pour une fonctionnalité dont le mode d'échec est une ligne périmée dans une liste. | |
| 42 | + | |
| 43 | +L'arbre relit donc à la demande, et l'éditeur choisit les moments dont il peut être sûr. Enregistrer un fichier en est un : c'est l'éditeur qui l'a fait, il le sait donc. `F5` et `Ctrl-R` sont l'autre, parce qu'une commande dans une fenêtre terminal est quelque chose que seul l'utilisateur sait terminée. Le rafraîchissement conserve la forme de l'arbre et ne relit que les dossiers réellement ouverts : il coûte ce qui est à l'écran, pas un parcours du projet. | |
| 44 | + | |
| 45 | +## Pourquoi l'arbre a ses propres clés de thème | |
| 46 | + | |
| 47 | +L'économie évidente était de le dessiner avec les clés `list.*` — un arbre est une liste, après tout, et cela n'aurait ajouté aucune clé que les thèmes utilisateur puissent manquer. | |
| 48 | + | |
| 49 | +Cela ne marche pas, et la raison mérite d'être consignée. `list.selected` est colorée pour ressortir sur un **dialogue**. Dans `turbo-classic` c'est blanc sur navy, et `window.body` est silver sur **navy** — un arbre dans une fenêtre aurait surligné sa ligne sélectionnée exactement dans la couleur de fond sur laquelle elle repose. La sélection aurait été invisible dans le thème que l'éditeur livre par défaut. | |
| 50 | + | |
| 51 | +D'où `tree.text`, `tree.directory`, `tree.selected` et `tree.unfocused`, et un test qui tient chaque thème livré à un contraste minimal entre la première et la troisième, de la même façon que les couleurs du curseur sont vérifiées. Un thème utilisateur qui n'en définit aucune retombe le long des points sur `default` : un arbre lisible, sans la distinction fichier/dossier, plutôt que rien du tout. | |
| 52 | + | |
| 53 | +## Liens avec le reste | |
| 54 | + | |
| 55 | +- Toutes les touches et toutes les règles, exactement : [Référence de l'arbre du projet](../reference/project-tree.md) | |
| 56 | +- L'utiliser : [Parcourir un projet et ouvrir des fichiers depuis un arbre](../how-to/browse-a-project.md) | |
| 57 | +- L'autre endroit où « le projet » est défini de la même façon : [Réglages de projet](project-settings.md) | |
| 58 | +- Où `filetree` se situe parmi les paquets : [Architecture](architecture.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,58 @@ | |||
| 1 | +# Arbre du projet — explication | ||
| 2 | + | ||
| 3 | +## De quoi s'agit-il ? | ||
| 4 | + | ||
| 5 | +`F9` ouvre une fenêtre listant les fichiers du projet, et `Entrée` sur l'un d'eux l'ouvre. Cette page traite des trois décisions contenues là-dedans : où l'arbre s'enracine, pourquoi c'est une fenêtre plutôt qu'un panneau sur le côté, et pourquoi il ne remarque pas tout seul l'apparition de fichiers. | ||
| 6 | + | ||
| 7 | +## Pourquoi la racine est le répertoire de travail | ||
| 8 | + | ||
| 9 | +L'éditeur contient déjà deux réponses différentes à « qu'est-ce que le projet ». | ||
| 10 | + | ||
| 11 | +Le serveur de langage reçoit le dossier du fichier ouvert, parce que c'est tout ce dont `golo lsp` a besoin : Golo n'a pas de manifeste de projet, et le serveur répond sur le fichier qu'on lui donne. Le fichier de réglages, lui, ne regarde qu'un seul endroit : `.turbo-golo/settings.toml` est cherché dans le répertoire de travail et nulle part ailleurs. | ||
| 12 | + | ||
| 13 | +L'arbre suit le fichier de réglages, et il vaut la peine de dire pourquoi l'*autre* règle était tentante. Enraciner l'arbre sur le dossier du fichier ouvert — ou remonter jusqu'à un `.git` — ferait qu'ouvrir un fichier de n'importe où dans un projet montrerait tout le projet, ce que fait habituellement un explorateur. Mais cela signifie aussi que la racine de l'arbre dépend d'un dossier auquel vous n'avez peut-être pas pensé, et cela cesse d'être prévisible dès qu'un dépôt contient plusieurs programmes — un monorepo vous montrerait le dossier auquel appartient le fichier que vous avez ouvert par hasard. | ||
| 14 | + | ||
| 15 | +La règle retenue est celle qui tient en une phrase et qui est vraie partout dans l'éditeur : **le projet est le dossier depuis lequel vous avez lancé l'éditeur.** Elle coûte quelque chose, et ce coût est nommé dans le [guide](../how-to/browse-a-project.md) : lancez depuis un sous-dossier et vous obtenez l'arbre de ce sous-dossier. La réponse est de lancer depuis la racine du projet, là où vous feriez `golo --test` et `git` de toute façon. | ||
| 16 | + | ||
| 17 | +## Pourquoi `.git` est masqué et rien d'autre | ||
| 18 | + | ||
| 19 | +Le dialogue Open masque toute entrée commençant par un point. Recopier cela ici était la chose évidente, et aurait été faux. | ||
| 20 | + | ||
| 21 | +`.turbo-golo/settings.toml` est un fichier que cet éditeur demande aux gens d'éditer — c'est la raison d'être de la coloration TOML. `.gitignore` et `.qlty/qlty.toml` sont eux aussi des fichiers du projet. Un arbre qui les cacherait rendrait la configuration de l'éditeur inaccessible depuis l'explorateur de fichiers de l'éditeur, ce qui est un endroit curieux où aboutir. | ||
| 22 | + | ||
| 23 | +`.git` diffère par nature et non par orthographe : rien à l'intérieur n'est fait pour être ouvert à la main, et il contient assez d'objets pour enterrer tout le reste dans la liste. Un seul nom, masqué pour une raison énonçable. Respecter aussi `.gitignore` a été envisagé et écarté pour l'instant : cela masquerait `bin/` et `release/`, ce qui serait réellement plus agréable, et cela coûte un moteur de motifs gitignore — négations, `**`, ancrage — qui est une fonctionnalité à part entière plutôt qu'un détail d'arbre. | ||
| 24 | + | ||
| 25 | +## Pourquoi une fenêtre, pas un panneau | ||
| 26 | + | ||
| 27 | +Tous les autres éditeurs mettent leur arbre de fichiers dans une bande fixe à gauche. C'était l'alternative, et elle a été écartée à cause de ce qu'elle aurait coûté au reste de l'éditeur. | ||
| 28 | + | ||
| 29 | +Un panneau ancré signifie que le bureau n'est plus un simple rectangle où vivent les fenêtres. `Desktop` aurait besoin d'une notion de bords réservés ; `Window.fitInto` et les modes de croissance devraient les respecter ; agrandir voudrait dire « tout le bureau sauf le panneau » ; la mise en mosaïque et en cascade devraient en tenir compte. C'est une modification des fondations de toute l'interface, pour un seul widget. | ||
| 30 | + | ||
| 31 | +En fenêtre ordinaire, l'arbre obtient tout gratuitement et se comporte comme le reste : `F6` l'atteint, `Alt-2` le remonte, `[x]` le ferme, `[■]` lui donne tout le bureau, **Window ▸ Tile** le met à côté de votre fichier. Rien dans `ui` n'a eu à changer. Si un panneau ancré est voulu plus tard, c'est une fonctionnalité de `ui` à concevoir pour elle-même, plutôt qu'à faire passer en douce avec un explorateur de fichiers. | ||
| 32 | + | ||
| 33 | +## Pourquoi il n'y en a qu'un | ||
| 34 | + | ||
| 35 | +Deux arbres sur le même projet seraient deux vues d'une même chose sans rien pour les distinguer, et le projet ne peut pas changer pendant que l'éditeur tourne — la racine est fixée au démarrage. `F9` sur un arbre ouvert le remonte donc au lieu d'en créer un autre, exactement comme ouvrir un fichier déjà ouvert remonte sa fenêtre. | ||
| 36 | + | ||
| 37 | +## Pourquoi il ne surveille pas le disque | ||
| 38 | + | ||
| 39 | +Un arbre qui remarquerait `gogolo build` produisant un exécutable serait meilleur. Le faire correctement signifie surveiller le système de fichiers, et en Go cela signifie `fsnotify` — une troisième dépendance, contre une bibliothèque qui s'en tient à deux depuis le début et qui traite l'ajout d'une dépendance comme une décision à défendre. | ||
| 40 | + | ||
| 41 | +Ce n'est pas non plus une petite dépendance en comportement : surveillances récursives, descripteurs épuisés sur les grosses arborescences, et une sémantique différente sur chaque plateforme — pour une fonctionnalité dont le mode d'échec est une ligne périmée dans une liste. | ||
| 42 | + | ||
| 43 | +L'arbre relit donc à la demande, et l'éditeur choisit les moments dont il peut être sûr. Enregistrer un fichier en est un : c'est l'éditeur qui l'a fait, il le sait donc. `F5` et `Ctrl-R` sont l'autre, parce qu'une commande dans une fenêtre terminal est quelque chose que seul l'utilisateur sait terminée. Le rafraîchissement conserve la forme de l'arbre et ne relit que les dossiers réellement ouverts : il coûte ce qui est à l'écran, pas un parcours du projet. | ||
| 44 | + | ||
| 45 | +## Pourquoi l'arbre a ses propres clés de thème | ||
| 46 | + | ||
| 47 | +L'économie évidente était de le dessiner avec les clés `list.*` — un arbre est une liste, après tout, et cela n'aurait ajouté aucune clé que les thèmes utilisateur puissent manquer. | ||
| 48 | + | ||
| 49 | +Cela ne marche pas, et la raison mérite d'être consignée. `list.selected` est colorée pour ressortir sur un **dialogue**. Dans `turbo-classic` c'est blanc sur navy, et `window.body` est silver sur **navy** — un arbre dans une fenêtre aurait surligné sa ligne sélectionnée exactement dans la couleur de fond sur laquelle elle repose. La sélection aurait été invisible dans le thème que l'éditeur livre par défaut. | ||
| 50 | + | ||
| 51 | +D'où `tree.text`, `tree.directory`, `tree.selected` et `tree.unfocused`, et un test qui tient chaque thème livré à un contraste minimal entre la première et la troisième, de la même façon que les couleurs du curseur sont vérifiées. Un thème utilisateur qui n'en définit aucune retombe le long des points sur `default` : un arbre lisible, sans la distinction fichier/dossier, plutôt que rien du tout. | ||
| 52 | + | ||
| 53 | +## Liens avec le reste | ||
| 54 | + | ||
| 55 | +- Toutes les touches et toutes les règles, exactement : [Référence de l'arbre du projet](../reference/project-tree.md) | ||
| 56 | +- L'utiliser : [Parcourir un projet et ouvrir des fichiers depuis un arbre](../how-to/browse-a-project.md) | ||
| 57 | +- L'autre endroit où « le projet » est défini de la même façon : [Réglages de projet](project-settings.md) | ||
| 58 | +- Où `filetree` se situe parmi les paquets : [Architecture](architecture.md) | ||
added
docs/fr/explanation/snippets.md +74 -0 | new file mode 100644 | ||
| @@ -0,0 +1,74 @@ | ||
| 1 | +# Snippets — explication | |
| 2 | + | |
| 3 | +## De quoi s'agit-il ? | |
| 4 | + | |
| 5 | +Un menu **Snippets** dont le contenu vient d'un fichier TOML, et un snippet choisi déposé dans le fichier que vous éditez. Cette page traite des décisions qui lui donnent sa forme : pourquoi le menu est reconstruit à chaque ouverture, pourquoi l'éditeur a gagné de vrais sous-menus pour lui, pourquoi l'insertion réindente — et, pour ce qui n'appartient qu'à Turbo Golo, pourquoi les snippets de départ sont écrits comme ils le sont. | |
| 6 | + | |
| 7 | +## Pourquoi le menu est construit au moment où il s'ouvre | |
| 8 | + | |
| 9 | +Tous les autres menus de l'éditeur sont décidés une fois, dans `New()`. Celui-ci ne peut pas l'être, et pour deux raisons indépendantes. | |
| 10 | + | |
| 11 | +La première est le fichier. Les snippets vivent dans du TOML, et tout l'intérêt est que vous l'éditiez — souvent dans cet éditeur, dans la fenêtre que l'entrée **Create snippets file** vient d'ouvrir pour vous. Un menu construit au démarrage montrerait l'état du fichier au lancement, et il faudrait redémarrer pour voir un snippet qu'on vient d'écrire. C'est le genre de friction qui fait qu'une fonctionnalité n'est pas utilisée du tout. | |
| 12 | + | |
| 13 | +La seconde est la fenêtre au premier plan. Le menu est filtré par ce que vous éditez, donc il change quand vous appuyez sur `F6`. Il n'existe aucun instant du démarrage où la réponse existe. | |
| 14 | + | |
| 15 | +`ui.Menu` a donc gagné un champ `OnOpen` : une fonction que la barre appelle juste avant de dérouler un menu, laissant son propriétaire regarnir `Items` d'abord. C'est le même mécanisme de communication ascendante que partout ailleurs dans ce code — un champ fonction, pas une interface — et il s'exécute exactement au moment où le contenu va être vu, pas plus souvent. | |
| 16 | + | |
| 17 | +## Pourquoi l'éditeur a gagné des sous-menus | |
| 18 | + | |
| 19 | +`ui.MenuItem` ne savait pas imbriquer, et l'ajouter a été la plus grosse pièce de ce travail : un second panneau à placer et à dessiner, des flèches qui signifient « plus profond » et « ressortir », le pointeur qui ouvre une branche au survol et la referme en la quittant, et une fermeture qui range les deux panneaux d'un coup. | |
| 20 | + | |
| 21 | +L'alternative était un seul panneau plat avec les groupes en intitulés grisés entre des filets. Cela fonctionne, ne demande rien de neuf, et s'effondre sur le cas même pour lequel la fonctionnalité existe : un projet de trente snippets donne un menu plus haut que le terminal. Un regroupement qui étiquette sans replier ne résout pas le problème qu'il semble résoudre. | |
| 22 | + | |
| 23 | +C'est délibérément **un seul niveau**. Le format est des groupes contenant des snippets — exactement un niveau — et une profondeur générale supposerait de remplacer les deux indices de la barre par un chemin, dans le widget dont dépendent déjà tous les dialogues et tous les tests de menu. C'est du travail spéculatif sur la partie la plus porteuse de l'interface. | |
| 24 | + | |
| 25 | +Deux détails du sous-menu méritent d'être nommés, parce qu'ils ont été choisis et non subis : | |
| 26 | + | |
| 27 | +- **Droite et gauche sont asymétriques avec Échap.** Droite ouvre une branche, ou passe au menu suivant quand l'entrée n'en a pas : elle signifie donc toujours « plus profond », où que l'on soit. Gauche *ressort* d'un sous-menu vers son parent, tandis qu'Échap referme tout le menu — parce qu'annuler doit vouloir dire annuler, de n'importe où. | |
| 28 | +- **Le panneau bascule à gauche, et sa largeur est aussi bornée.** Un sous-menu qui dépasserait le bord droit est dessiné de l'autre côté de son parent. Basculer ne suffit pas : un panneau plus large que le terminal ne peut pas être rendu visible en le déplaçant, donc la largeur est bornée aussi et les intitulés longs sont coupés par le peintre. Un cadre sans bord droit paraît cassé d'une façon dont un intitulé tronqué ne l'est pas. | |
| 29 | + | |
| 30 | +## Pourquoi l'insertion réindente | |
| 31 | + | |
| 32 | +Un snippet est du texte, et l'implémentation évidente est de l'insérer. C'est juste pour une seule ligne et faux pour tout le reste, c'est-à-dire pour l'essentiel de ce que les gens gardent en snippets. | |
| 33 | + | |
| 34 | +Déposé tel quel, un corps multi-ligne repart en colonne zéro. Inséré dans une fonction, dans une boucle `foreach`, dans un bloc `try` — là où l'on insère justement un `match` — le résultat est un texte qu'aucun lecteur n'accepte, et Golo n'a pas de formateur pour le remettre d'équerre après coup : ce qui est inséré reste tel quel. La première chose qu'on fait est de le réindenter à la main, et une fonctionnalité dont la sortie doit être corrigée chaque fois ne fait gagner de temps à personne. | |
| 35 | + | |
| 36 | +Les lignes après la première reçoivent donc l'indentation de la ligne où était le curseur. Cela recopie ce que le fichier emploie déjà — tabulations ou espaces, en telle quantité — plutôt que d'imposer un choix, ce qui compte dans un projet à l'histoire mêlée. | |
| 37 | + | |
| 38 | +Deux décisions plus petites à l'intérieur : | |
| 39 | + | |
| 40 | +- **Une ligne vide du corps reste vide.** La compléter jusqu'à l'indentation y mettrait des espaces en fin de ligne — du bruit dans le diff de l'enregistrement suivant, que rien ne viendrait nettoyer puisqu'aucun formateur ne repasse derrière. | |
| 41 | +- **C'est une seule annulation.** Un snippet est une seule action pour qui l'a choisi, donc `Ctrl-Z` doit tout reprendre. Cela découle de faire toute l'insertion en un seul `ReplaceRange`, la règle que le buffer impose déjà à toute autre modification. | |
| 42 | + | |
| 43 | +Les emplacements et les tabulations successives — `${1:nom}` et le passage de l'un à l'autre — ont été envisagés et laissés de côté. C'est une seconde fonctionnalité, avec son propre état à maintenir à travers les modifications, alors que ce qui était demandé est du texte réutilisable. | |
| 44 | + | |
| 45 | +## Pourquoi deux fichiers, et pourquoi le projet gagne | |
| 46 | + | |
| 47 | +Vos snippets vous appartiennent et doivent vous suivre d'un projet à l'autre ; ceux d'un projet lui appartiennent et doivent arriver avec un clone. Ni l'un ni l'autre n'est la réponse complète, donc les deux sont lus. | |
| 48 | + | |
| 49 | +Quand un nom entre en conflit dans le même groupe, celui du projet remplace le vôtre. C'est le plus spécifique des deux énoncés, et c'est celui dont une équipe a convenu — la même raison qui fait qu'un drapeau `-theme` l'emporte sur le réglage d'un projet, tandis que le réglage d'un projet l'emporte sur le défaut intégré. | |
| 50 | + | |
| 51 | +## Pourquoi un fichier illisible est bruyant | |
| 52 | + | |
| 53 | +Une faute de frappe dans le TOML pourrait faire disparaître tous les snippets en silence et laisser un menu ne contenant que **Create snippets file** — ce qui ressemble exactement à un projet sans snippets, et vous envoie créer un fichier que vous avez déjà. | |
| 54 | + | |
| 55 | +Le menu affiche donc un `Cannot read snippets` grisé là où les groupes seraient. Il ne peut pas être choisi, il est là où vous regardiez, et l'entrée de création reste en dessous : il y a une issue dans les deux cas. | |
| 56 | + | |
| 57 | +## Pourquoi les snippets de départ sont indentés de deux espaces | |
| 58 | + | |
| 59 | +Ceci n'appartient qu'à Turbo Golo. Le fichier de départ que **Create snippets file** écrit contient douze snippets Golo — `module`, `main`, `function`, `closure`, `struct`, `union`, `augment`, `match`, `foreach`, `for`, `try`, `comprehension` — et il fallait choisir comment indenter leurs corps. | |
| 60 | + | |
| 61 | +Dans un éditeur dont le langage a un formateur, la réponse est imposée : on écrit ce que le formateur écrirait, sinon le premier passage de l'outil réécrit le snippet. Golo n'a pas de formateur. La seule autorité qui reste est la convention, et tous les exemples de GoloScript — sa documentation, ses propres modèles — emploient deux espaces. C'est donc deux espaces, parce que tous les exemples le font et qu'il n'y a rien pour dire le contraire. Un test tient les corps à cette règle. | |
| 62 | + | |
| 63 | +## Pourquoi les corps sont des chaînes littérales TOML | |
| 64 | + | |
| 65 | +Même raison d'être locale. Une chaîne Golo porte `\n` et `\"` comme une chaîne Go, et le snippet `try` en contient justement — `println("caught: \"" + e + "\"")`. Écrit dans une chaîne TOML basique, `"""…"""`, ces échappements seraient résolus par l'analyseur TOML avant que l'éditeur ne voie le corps : le snippet arriverait dans le fichier avec un vrai guillemet là où le code voulait un guillemet échappé, et il ne s'exécuterait plus. | |
| 66 | + | |
| 67 | +Les corps Golo sont donc écrits en chaînes *littérales*, `'''…'''`, où une barre oblique inverse est une barre oblique inverse. Ce qui est dans le fichier de snippets est exactement ce qui arrive dans le fichier Golo, ce qui est la seule chose qu'on attend d'un snippet. | |
| 68 | + | |
| 69 | +## Liens avec le reste | |
| 70 | + | |
| 71 | +- Toutes les clés et toutes les règles : [Référence des snippets](../reference/snippets.md) | |
| 72 | +- Les mettre en place : [Insérer des snippets depuis un menu](../how-to/use-snippets.md) | |
| 73 | +- L'autre fichier du même dossier : [Réglages de projet](project-settings.md) | |
| 74 | +- Les noms de langages qu'emploie `languages` : [Langages colorés](../reference/languages.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,74 @@ | |||
| 1 | +# Snippets — explication | ||
| 2 | + | ||
| 3 | +## De quoi s'agit-il ? | ||
| 4 | + | ||
| 5 | +Un menu **Snippets** dont le contenu vient d'un fichier TOML, et un snippet choisi déposé dans le fichier que vous éditez. Cette page traite des décisions qui lui donnent sa forme : pourquoi le menu est reconstruit à chaque ouverture, pourquoi l'éditeur a gagné de vrais sous-menus pour lui, pourquoi l'insertion réindente — et, pour ce qui n'appartient qu'à Turbo Golo, pourquoi les snippets de départ sont écrits comme ils le sont. | ||
| 6 | + | ||
| 7 | +## Pourquoi le menu est construit au moment où il s'ouvre | ||
| 8 | + | ||
| 9 | +Tous les autres menus de l'éditeur sont décidés une fois, dans `New()`. Celui-ci ne peut pas l'être, et pour deux raisons indépendantes. | ||
| 10 | + | ||
| 11 | +La première est le fichier. Les snippets vivent dans du TOML, et tout l'intérêt est que vous l'éditiez — souvent dans cet éditeur, dans la fenêtre que l'entrée **Create snippets file** vient d'ouvrir pour vous. Un menu construit au démarrage montrerait l'état du fichier au lancement, et il faudrait redémarrer pour voir un snippet qu'on vient d'écrire. C'est le genre de friction qui fait qu'une fonctionnalité n'est pas utilisée du tout. | ||
| 12 | + | ||
| 13 | +La seconde est la fenêtre au premier plan. Le menu est filtré par ce que vous éditez, donc il change quand vous appuyez sur `F6`. Il n'existe aucun instant du démarrage où la réponse existe. | ||
| 14 | + | ||
| 15 | +`ui.Menu` a donc gagné un champ `OnOpen` : une fonction que la barre appelle juste avant de dérouler un menu, laissant son propriétaire regarnir `Items` d'abord. C'est le même mécanisme de communication ascendante que partout ailleurs dans ce code — un champ fonction, pas une interface — et il s'exécute exactement au moment où le contenu va être vu, pas plus souvent. | ||
| 16 | + | ||
| 17 | +## Pourquoi l'éditeur a gagné des sous-menus | ||
| 18 | + | ||
| 19 | +`ui.MenuItem` ne savait pas imbriquer, et l'ajouter a été la plus grosse pièce de ce travail : un second panneau à placer et à dessiner, des flèches qui signifient « plus profond » et « ressortir », le pointeur qui ouvre une branche au survol et la referme en la quittant, et une fermeture qui range les deux panneaux d'un coup. | ||
| 20 | + | ||
| 21 | +L'alternative était un seul panneau plat avec les groupes en intitulés grisés entre des filets. Cela fonctionne, ne demande rien de neuf, et s'effondre sur le cas même pour lequel la fonctionnalité existe : un projet de trente snippets donne un menu plus haut que le terminal. Un regroupement qui étiquette sans replier ne résout pas le problème qu'il semble résoudre. | ||
| 22 | + | ||
| 23 | +C'est délibérément **un seul niveau**. Le format est des groupes contenant des snippets — exactement un niveau — et une profondeur générale supposerait de remplacer les deux indices de la barre par un chemin, dans le widget dont dépendent déjà tous les dialogues et tous les tests de menu. C'est du travail spéculatif sur la partie la plus porteuse de l'interface. | ||
| 24 | + | ||
| 25 | +Deux détails du sous-menu méritent d'être nommés, parce qu'ils ont été choisis et non subis : | ||
| 26 | + | ||
| 27 | +- **Droite et gauche sont asymétriques avec Échap.** Droite ouvre une branche, ou passe au menu suivant quand l'entrée n'en a pas : elle signifie donc toujours « plus profond », où que l'on soit. Gauche *ressort* d'un sous-menu vers son parent, tandis qu'Échap referme tout le menu — parce qu'annuler doit vouloir dire annuler, de n'importe où. | ||
| 28 | +- **Le panneau bascule à gauche, et sa largeur est aussi bornée.** Un sous-menu qui dépasserait le bord droit est dessiné de l'autre côté de son parent. Basculer ne suffit pas : un panneau plus large que le terminal ne peut pas être rendu visible en le déplaçant, donc la largeur est bornée aussi et les intitulés longs sont coupés par le peintre. Un cadre sans bord droit paraît cassé d'une façon dont un intitulé tronqué ne l'est pas. | ||
| 29 | + | ||
| 30 | +## Pourquoi l'insertion réindente | ||
| 31 | + | ||
| 32 | +Un snippet est du texte, et l'implémentation évidente est de l'insérer. C'est juste pour une seule ligne et faux pour tout le reste, c'est-à-dire pour l'essentiel de ce que les gens gardent en snippets. | ||
| 33 | + | ||
| 34 | +Déposé tel quel, un corps multi-ligne repart en colonne zéro. Inséré dans une fonction, dans une boucle `foreach`, dans un bloc `try` — là où l'on insère justement un `match` — le résultat est un texte qu'aucun lecteur n'accepte, et Golo n'a pas de formateur pour le remettre d'équerre après coup : ce qui est inséré reste tel quel. La première chose qu'on fait est de le réindenter à la main, et une fonctionnalité dont la sortie doit être corrigée chaque fois ne fait gagner de temps à personne. | ||
| 35 | + | ||
| 36 | +Les lignes après la première reçoivent donc l'indentation de la ligne où était le curseur. Cela recopie ce que le fichier emploie déjà — tabulations ou espaces, en telle quantité — plutôt que d'imposer un choix, ce qui compte dans un projet à l'histoire mêlée. | ||
| 37 | + | ||
| 38 | +Deux décisions plus petites à l'intérieur : | ||
| 39 | + | ||
| 40 | +- **Une ligne vide du corps reste vide.** La compléter jusqu'à l'indentation y mettrait des espaces en fin de ligne — du bruit dans le diff de l'enregistrement suivant, que rien ne viendrait nettoyer puisqu'aucun formateur ne repasse derrière. | ||
| 41 | +- **C'est une seule annulation.** Un snippet est une seule action pour qui l'a choisi, donc `Ctrl-Z` doit tout reprendre. Cela découle de faire toute l'insertion en un seul `ReplaceRange`, la règle que le buffer impose déjà à toute autre modification. | ||
| 42 | + | ||
| 43 | +Les emplacements et les tabulations successives — `${1:nom}` et le passage de l'un à l'autre — ont été envisagés et laissés de côté. C'est une seconde fonctionnalité, avec son propre état à maintenir à travers les modifications, alors que ce qui était demandé est du texte réutilisable. | ||
| 44 | + | ||
| 45 | +## Pourquoi deux fichiers, et pourquoi le projet gagne | ||
| 46 | + | ||
| 47 | +Vos snippets vous appartiennent et doivent vous suivre d'un projet à l'autre ; ceux d'un projet lui appartiennent et doivent arriver avec un clone. Ni l'un ni l'autre n'est la réponse complète, donc les deux sont lus. | ||
| 48 | + | ||
| 49 | +Quand un nom entre en conflit dans le même groupe, celui du projet remplace le vôtre. C'est le plus spécifique des deux énoncés, et c'est celui dont une équipe a convenu — la même raison qui fait qu'un drapeau `-theme` l'emporte sur le réglage d'un projet, tandis que le réglage d'un projet l'emporte sur le défaut intégré. | ||
| 50 | + | ||
| 51 | +## Pourquoi un fichier illisible est bruyant | ||
| 52 | + | ||
| 53 | +Une faute de frappe dans le TOML pourrait faire disparaître tous les snippets en silence et laisser un menu ne contenant que **Create snippets file** — ce qui ressemble exactement à un projet sans snippets, et vous envoie créer un fichier que vous avez déjà. | ||
| 54 | + | ||
| 55 | +Le menu affiche donc un `Cannot read snippets` grisé là où les groupes seraient. Il ne peut pas être choisi, il est là où vous regardiez, et l'entrée de création reste en dessous : il y a une issue dans les deux cas. | ||
| 56 | + | ||
| 57 | +## Pourquoi les snippets de départ sont indentés de deux espaces | ||
| 58 | + | ||
| 59 | +Ceci n'appartient qu'à Turbo Golo. Le fichier de départ que **Create snippets file** écrit contient douze snippets Golo — `module`, `main`, `function`, `closure`, `struct`, `union`, `augment`, `match`, `foreach`, `for`, `try`, `comprehension` — et il fallait choisir comment indenter leurs corps. | ||
| 60 | + | ||
| 61 | +Dans un éditeur dont le langage a un formateur, la réponse est imposée : on écrit ce que le formateur écrirait, sinon le premier passage de l'outil réécrit le snippet. Golo n'a pas de formateur. La seule autorité qui reste est la convention, et tous les exemples de GoloScript — sa documentation, ses propres modèles — emploient deux espaces. C'est donc deux espaces, parce que tous les exemples le font et qu'il n'y a rien pour dire le contraire. Un test tient les corps à cette règle. | ||
| 62 | + | ||
| 63 | +## Pourquoi les corps sont des chaînes littérales TOML | ||
| 64 | + | ||
| 65 | +Même raison d'être locale. Une chaîne Golo porte `\n` et `\"` comme une chaîne Go, et le snippet `try` en contient justement — `println("caught: \"" + e + "\"")`. Écrit dans une chaîne TOML basique, `"""…"""`, ces échappements seraient résolus par l'analyseur TOML avant que l'éditeur ne voie le corps : le snippet arriverait dans le fichier avec un vrai guillemet là où le code voulait un guillemet échappé, et il ne s'exécuterait plus. | ||
| 66 | + | ||
| 67 | +Les corps Golo sont donc écrits en chaînes *littérales*, `'''…'''`, où une barre oblique inverse est une barre oblique inverse. Ce qui est dans le fichier de snippets est exactement ce qui arrive dans le fichier Golo, ce qui est la seule chose qu'on attend d'un snippet. | ||
| 68 | + | ||
| 69 | +## Liens avec le reste | ||
| 70 | + | ||
| 71 | +- Toutes les clés et toutes les règles : [Référence des snippets](../reference/snippets.md) | ||
| 72 | +- Les mettre en place : [Insérer des snippets depuis un menu](../how-to/use-snippets.md) | ||
| 73 | +- L'autre fichier du même dossier : [Réglages de projet](project-settings.md) | ||
| 74 | +- Les noms de langages qu'emploie `languages` : [Langages colorés](../reference/languages.md) | ||
added
docs/fr/explanation/terminal-windows.md +72 -0 | new file mode 100644 | ||
| @@ -0,0 +1,72 @@ | ||
| 1 | +# Fenêtres terminal — explication | |
| 2 | + | |
| 3 | +## De quoi s'agit-il ? | |
| 4 | + | |
| 5 | +`F8` ouvre une fenêtre contenant un shell. Cette phrase masque l'essentiel du travail : pour mettre un shell dans une fenêtre, un éditeur doit devenir un émulateur de terminal. Cette page raconte ce que cela a impliqué, et quelles solutions moins coûteuses ont été écartées en chemin. | |
| 6 | + | |
| 7 | +## Pourquoi un vrai pseudo-terminal | |
| 8 | + | |
| 9 | +La version bon marché évidente consiste à lancer une commande avec `exec.Command`, à capturer sa sortie et à l'afficher dans un panneau en lecture seule. Beaucoup d'éditeurs livrent exactement cela, et cela échoue précisément sur ce pour quoi on veut un terminal. | |
| 10 | + | |
| 11 | +Un programme se comporte différemment quand sa sortie est un tube plutôt qu'un terminal. `golo --test` abandonne ses couleurs. `git log` ne pagine pas. `ls` affiche un nom par ligne. Rien d'interactif ne fonctionne : ni `vim`, ni `ssh`, ni le REPL de `golo`, ni `git rebase -i`, ni la réponse à une invite, ni `Ctrl-C` — sans terminal de contrôle, il n'y a aucun signal à envoyer. | |
| 12 | + | |
| 13 | +Le shell reçoit donc un vrai pseudo-terminal : `/dev/ptmx` sur les deux plateformes supportées, le fils dans une session à lui avec l'esclave comme terminal de contrôle, et `TIOCSWINSZ` à chaque redimensionnement de la fenêtre. Cela offre gratuitement le contrôle de tâches, `isatty`, `SIGWINCH` et la couleur, parce que ce sont les mêmes mécanismes que ceux de tous les autres terminaux. | |
| 14 | + | |
| 15 | +Le prix à payer est que l'éditeur doit ensuite relire ce qu'un terminal est censé comprendre — c'est-à-dire l'émulateur. | |
| 16 | + | |
| 17 | +## Pourquoi écrire l'émulateur plutôt que d'en emprunter un | |
| 18 | + | |
| 19 | +Go dispose de bibliothèques d'émulation de terminal. En prendre une aurait signifié une troisième dépendance, dans une bibliothèque qui en a exactement deux et qui affiche une réticence assumée à en ajouter une troisième. | |
| 20 | + | |
| 21 | +Ce que l'on met en balance n'est pas « émulateur » contre « pas d'émulateur », mais *quelle quantité* d'émulateur. Ce dont ont besoin un shell, `golo --test`, `git`, `less`, `htop` et `vim` forme une liste bien délimitée : déplacement du curseur, la famille effacement / insertion-suppression, une région de défilement, SGR dans ses trois profondeurs de couleur, l'écran alternatif, le retour à la ligne automatique, la visibilité du curseur et les touches curseur application. Cela représente environ six cents lignes, c'est écrit noir sur blanc dans ECMA-48, et cela se teste en écrivant des octets en entrée et en lisant une grille en sortie — sans shell, sans temporisation, sans écran. | |
| 22 | + | |
| 23 | +À comparer avec ce qu'apporte une bibliothèque généraliste : jeux de caractères, protocoles de rapport souris, sixel, collage entre crochets, rapports d'état DEC. Tout cela est réel, rien n'est nécessaire ici, et tout cela constitue de la surface à maintenir. | |
| 24 | + | |
| 25 | +L'émulateur est donc écrit à la main et volontairement partiel, et la [référence](../reference/terminal.md) dit exactement où il s'arrête. Un programme qui demande quelque chose d'absent obtient le silence plutôt que de la corruption, ce qui est le bon mode d'échec : `htop` s'affiche, la sortie `sixel` n'apparaît simplement pas. | |
| 26 | + | |
| 27 | +## À qui revient la touche | |
| 28 | + | |
| 29 | +C'est la décision qui pèse le plus sur la sensation d'usage de l'éditeur, et la première version s'était trompée. | |
| 30 | + | |
| 31 | +Les raccourcis globaux de l'éditeur sont examinés avant que la fenêtre du premier plan ne voie quoi que ce soit. C'est juste pour un éditeur, et faux dès l'instant où cette fenêtre est un shell, parce que les deux revendiquent les mêmes touches. `Ctrl-W` ferme une fenêtre dans Turbo C et supprime un mot dans tous les shells. `Ctrl-F` est Rechercher ici et avancer-d'un-caractère dans readline. `Ctrl-C` est copier, et aussi le seul moyen d'arrêter une commande emballée. | |
| 32 | + | |
| 33 | +La règle retenue inverse l'ordre habituel, mais uniquement pour les touches réellement disputées : | |
| 34 | + | |
| 35 | +**Un terminal ayant le focus reçoit tout, sauf les touches de fonction, `Alt-X` et `Alt-0`…`Alt-9`.** | |
| 36 | + | |
| 37 | +Ces exceptions ne sont pas un compromis entre les deux revendications — ce sont la *sortie*. Un programme plein écran comme `vim` recouvre la fenêtre et s'empare de la souris ; sans touche réservée, il n'y aurait aucun moyen d'atteindre la barre de menus, de changer de fenêtre ou de quitter l'éditeur sans d'abord quitter le programme. Les touches de fonction sont la réservation naturelle parce que c'est vers elles qu'un utilisateur de terminal se tourne le moins, et `Alt-X` parce que quitter un éditeur ne devrait jamais faire de doute. | |
| 38 | + | |
| 39 | +Ce que cela coûte est réel et mérite d'être nommé : `Alt-B` et `Alt-F` atteignent le shell, donc le déplacement par mot de readline fonctionne, mais un programme dans une fenêtre terminal ne verra jamais `F1`…`F12`. Le menu par touches de fonction de `htop` est inaccessible. C'est l'arbitrage, et il a été rendu en faveur du fait de toujours pouvoir sortir. | |
| 40 | + | |
| 41 | +## Pourquoi fermer un terminal ne demande rien | |
| 42 | + | |
| 43 | +Fermer un fichier modifié demande s'il faut l'enregistrer. Fermer un terminal ne demande rien du tout, et cette asymétrie est délibérée. | |
| 44 | + | |
| 45 | +Une fenêtre au travail non enregistré contient quelque chose qui serait *perdu*. Un terminal contient un processus en cours, et fermer la fenêtre est la façon ordinaire de dire qu'on en a fini — comme on ferme l'onglet d'un émulateur de terminal. Demander « êtes-vous sûr ? » à chaque fois désapprendrait la réponse à quiconque, ce qui est le problème général des confirmations qui se déclenchent sur le cas courant. | |
| 46 | + | |
| 47 | +Quitter l'éditeur ferme tous les terminaux pour la même raison, en sens inverse : une fenêtre est la seule prise sur ces shells, donc les laisser survivre à l'éditeur abandonnerait des processus que plus rien ne peut atteindre. | |
| 48 | + | |
| 49 | +## Pourquoi les redessins sont cadencés | |
| 50 | + | |
| 51 | +Le shell écrit depuis une goroutine à lui ; l'éditeur dessine depuis la principale. Réveiller la boucle d'événements à chaque bloc de sortie semblait évident et se trompait deux fois. | |
| 52 | + | |
| 53 | +Une commande bavarde écrit bien plus vite qu'un écran ne peut être utilement repeint : la plupart de ces redessins sont donc du gaspillage. Pire, le mécanisme de réveil de la boucle depuis une autre goroutine est le `PostEvent` de tcell, qui **jette** les événements quand sa file est pleine — de sorte que la rafale qui a le plus besoin d'un redessin est justement celle dont le réveil final est perdu, et la fenêtre se fige en pleine exécution sur un texte périmé. Ce bug exact avait déjà été rencontré une fois ailleurs dans cet éditeur, du côté du serveur de langage. | |
| 54 | + | |
| 55 | +La vue positionne donc un drapeau, et une horloge demande un redessin soixante fois par seconde tant que le drapeau est levé. Un réveil perdu ne peut rien bloquer, puisque le tic suivant est à seize millisecondes. | |
| 56 | + | |
| 57 | +## Windows : une pseudo-console, et pourquoi c'est un fichier à part | |
| 58 | + | |
| 59 | +Les pseudo-terminaux sont la seule partie non portable de tout ceci. Linux et macOS passent tous deux par `/dev/ptmx` et ne diffèrent que par l'`ioctl` qui accorde l'esclave. Windows n'a rien de tel : il a des **pseudo-consoles** — ConPTY, depuis Windows 10 version 1809 — un objet détenu par `conhost.exe` et relié à deux tubes de l'éditeur. Ce que le shell affiche arrive sur l'un des tubes sous la forme des mêmes séquences VT qu'un shell Unix écrit dans un pty, ce qui est la raison pour laquelle l'émulateur de ce côté n'a eu besoin d'aucun code Windows ; ce que l'éditeur écrit dans l'autre tube parvient au shell comme des frappes de touches. | |
| 60 | + | |
| 61 | +Trois choses en ont fait un fichier à part plutôt qu'une variante du fichier Unix. Le processus doit être créé à la main, parce que l'attacher à une pseudo-console exige un enregistrement de démarrage étendu que l'`os/exec` de Go ne sait pas porter. Le shell est `%COMSPEC%` — cmd.exe — plutôt que `$SHELL`, et cmd.exe lit sa ligne de commande selon ses propres règles : la ligne qui lance une commande du menu est donc composée pour lui mot pour mot, la commande entre une seule paire de guillemets, au lieu d'être échappée comme tout autre programme l'attend. Et `conhost.exe` garde le tube de sortie ouvert jusqu'à la fermeture de la console, quoi que fasse le shell ; une goroutine attend donc la fin du shell puis ferme la console — c'est ce qui transforme une commande terminée en la fin d'entrée sur laquelle la fenêtre compte pour le dire. Le contrôle de tâches est celui de cmd.exe et non du noyau : `Ctrl-C` interrompt le programme en cours comme il le ferait dans une fenêtre de console. | |
| 62 | + | |
| 63 | +Les fichiers par plateforme restent séparés pour que chaque plateforme ait une implémentation honnête derrière une petite interface, et qu'une plateforme qui n'a ni l'un ni l'autre — les BSD, aujourd'hui — reçoive `ErrUnsupported`, que `F8` le dise clairement, et que rien d'autre dans l'éditeur ne soit affecté. | |
| 64 | + | |
| 65 | +**Le chemin Windows a été compilé et vérifié, pas exécuté.** turbo-core est développé sous Linux et son auteur travaille sous macOS. Les parties pures — le bloc d'environnement, la ligne de commande que veut cmd.exe — sont testées unitairement sur toute plateforme, et les appels à l'API compilent et passent `go vet` sous `GOOS=windows` ; personne n'a encore appuyé sur `F8` sur une machine Windows. [Le guide](../how-to/use-a-terminal.md) dit quoi essayer en premier. | |
| 66 | + | |
| 67 | +## Liens avec le reste | |
| 68 | + | |
| 69 | +- La liste exacte de ce qui est implémenté : [référence des fenêtres terminal](../reference/terminal.md) | |
| 70 | +- En utiliser une : [Lancer des commandes shell sans quitter l'éditeur](../how-to/use-a-terminal.md) | |
| 71 | +- Où `terminal` se situe parmi les paquets, et pourquoi le graphe est orienté : [Architecture](architecture.md) | |
| 72 | +- Le décompte de dépendances que cette page ne cesse d'invoquer : [Décisions de conception](design-decisions.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,72 @@ | |||
| 1 | +# Fenêtres terminal — explication | ||
| 2 | + | ||
| 3 | +## De quoi s'agit-il ? | ||
| 4 | + | ||
| 5 | +`F8` ouvre une fenêtre contenant un shell. Cette phrase masque l'essentiel du travail : pour mettre un shell dans une fenêtre, un éditeur doit devenir un émulateur de terminal. Cette page raconte ce que cela a impliqué, et quelles solutions moins coûteuses ont été écartées en chemin. | ||
| 6 | + | ||
| 7 | +## Pourquoi un vrai pseudo-terminal | ||
| 8 | + | ||
| 9 | +La version bon marché évidente consiste à lancer une commande avec `exec.Command`, à capturer sa sortie et à l'afficher dans un panneau en lecture seule. Beaucoup d'éditeurs livrent exactement cela, et cela échoue précisément sur ce pour quoi on veut un terminal. | ||
| 10 | + | ||
| 11 | +Un programme se comporte différemment quand sa sortie est un tube plutôt qu'un terminal. `golo --test` abandonne ses couleurs. `git log` ne pagine pas. `ls` affiche un nom par ligne. Rien d'interactif ne fonctionne : ni `vim`, ni `ssh`, ni le REPL de `golo`, ni `git rebase -i`, ni la réponse à une invite, ni `Ctrl-C` — sans terminal de contrôle, il n'y a aucun signal à envoyer. | ||
| 12 | + | ||
| 13 | +Le shell reçoit donc un vrai pseudo-terminal : `/dev/ptmx` sur les deux plateformes supportées, le fils dans une session à lui avec l'esclave comme terminal de contrôle, et `TIOCSWINSZ` à chaque redimensionnement de la fenêtre. Cela offre gratuitement le contrôle de tâches, `isatty`, `SIGWINCH` et la couleur, parce que ce sont les mêmes mécanismes que ceux de tous les autres terminaux. | ||
| 14 | + | ||
| 15 | +Le prix à payer est que l'éditeur doit ensuite relire ce qu'un terminal est censé comprendre — c'est-à-dire l'émulateur. | ||
| 16 | + | ||
| 17 | +## Pourquoi écrire l'émulateur plutôt que d'en emprunter un | ||
| 18 | + | ||
| 19 | +Go dispose de bibliothèques d'émulation de terminal. En prendre une aurait signifié une troisième dépendance, dans une bibliothèque qui en a exactement deux et qui affiche une réticence assumée à en ajouter une troisième. | ||
| 20 | + | ||
| 21 | +Ce que l'on met en balance n'est pas « émulateur » contre « pas d'émulateur », mais *quelle quantité* d'émulateur. Ce dont ont besoin un shell, `golo --test`, `git`, `less`, `htop` et `vim` forme une liste bien délimitée : déplacement du curseur, la famille effacement / insertion-suppression, une région de défilement, SGR dans ses trois profondeurs de couleur, l'écran alternatif, le retour à la ligne automatique, la visibilité du curseur et les touches curseur application. Cela représente environ six cents lignes, c'est écrit noir sur blanc dans ECMA-48, et cela se teste en écrivant des octets en entrée et en lisant une grille en sortie — sans shell, sans temporisation, sans écran. | ||
| 22 | + | ||
| 23 | +À comparer avec ce qu'apporte une bibliothèque généraliste : jeux de caractères, protocoles de rapport souris, sixel, collage entre crochets, rapports d'état DEC. Tout cela est réel, rien n'est nécessaire ici, et tout cela constitue de la surface à maintenir. | ||
| 24 | + | ||
| 25 | +L'émulateur est donc écrit à la main et volontairement partiel, et la [référence](../reference/terminal.md) dit exactement où il s'arrête. Un programme qui demande quelque chose d'absent obtient le silence plutôt que de la corruption, ce qui est le bon mode d'échec : `htop` s'affiche, la sortie `sixel` n'apparaît simplement pas. | ||
| 26 | + | ||
| 27 | +## À qui revient la touche | ||
| 28 | + | ||
| 29 | +C'est la décision qui pèse le plus sur la sensation d'usage de l'éditeur, et la première version s'était trompée. | ||
| 30 | + | ||
| 31 | +Les raccourcis globaux de l'éditeur sont examinés avant que la fenêtre du premier plan ne voie quoi que ce soit. C'est juste pour un éditeur, et faux dès l'instant où cette fenêtre est un shell, parce que les deux revendiquent les mêmes touches. `Ctrl-W` ferme une fenêtre dans Turbo C et supprime un mot dans tous les shells. `Ctrl-F` est Rechercher ici et avancer-d'un-caractère dans readline. `Ctrl-C` est copier, et aussi le seul moyen d'arrêter une commande emballée. | ||
| 32 | + | ||
| 33 | +La règle retenue inverse l'ordre habituel, mais uniquement pour les touches réellement disputées : | ||
| 34 | + | ||
| 35 | +**Un terminal ayant le focus reçoit tout, sauf les touches de fonction, `Alt-X` et `Alt-0`…`Alt-9`.** | ||
| 36 | + | ||
| 37 | +Ces exceptions ne sont pas un compromis entre les deux revendications — ce sont la *sortie*. Un programme plein écran comme `vim` recouvre la fenêtre et s'empare de la souris ; sans touche réservée, il n'y aurait aucun moyen d'atteindre la barre de menus, de changer de fenêtre ou de quitter l'éditeur sans d'abord quitter le programme. Les touches de fonction sont la réservation naturelle parce que c'est vers elles qu'un utilisateur de terminal se tourne le moins, et `Alt-X` parce que quitter un éditeur ne devrait jamais faire de doute. | ||
| 38 | + | ||
| 39 | +Ce que cela coûte est réel et mérite d'être nommé : `Alt-B` et `Alt-F` atteignent le shell, donc le déplacement par mot de readline fonctionne, mais un programme dans une fenêtre terminal ne verra jamais `F1`…`F12`. Le menu par touches de fonction de `htop` est inaccessible. C'est l'arbitrage, et il a été rendu en faveur du fait de toujours pouvoir sortir. | ||
| 40 | + | ||
| 41 | +## Pourquoi fermer un terminal ne demande rien | ||
| 42 | + | ||
| 43 | +Fermer un fichier modifié demande s'il faut l'enregistrer. Fermer un terminal ne demande rien du tout, et cette asymétrie est délibérée. | ||
| 44 | + | ||
| 45 | +Une fenêtre au travail non enregistré contient quelque chose qui serait *perdu*. Un terminal contient un processus en cours, et fermer la fenêtre est la façon ordinaire de dire qu'on en a fini — comme on ferme l'onglet d'un émulateur de terminal. Demander « êtes-vous sûr ? » à chaque fois désapprendrait la réponse à quiconque, ce qui est le problème général des confirmations qui se déclenchent sur le cas courant. | ||
| 46 | + | ||
| 47 | +Quitter l'éditeur ferme tous les terminaux pour la même raison, en sens inverse : une fenêtre est la seule prise sur ces shells, donc les laisser survivre à l'éditeur abandonnerait des processus que plus rien ne peut atteindre. | ||
| 48 | + | ||
| 49 | +## Pourquoi les redessins sont cadencés | ||
| 50 | + | ||
| 51 | +Le shell écrit depuis une goroutine à lui ; l'éditeur dessine depuis la principale. Réveiller la boucle d'événements à chaque bloc de sortie semblait évident et se trompait deux fois. | ||
| 52 | + | ||
| 53 | +Une commande bavarde écrit bien plus vite qu'un écran ne peut être utilement repeint : la plupart de ces redessins sont donc du gaspillage. Pire, le mécanisme de réveil de la boucle depuis une autre goroutine est le `PostEvent` de tcell, qui **jette** les événements quand sa file est pleine — de sorte que la rafale qui a le plus besoin d'un redessin est justement celle dont le réveil final est perdu, et la fenêtre se fige en pleine exécution sur un texte périmé. Ce bug exact avait déjà été rencontré une fois ailleurs dans cet éditeur, du côté du serveur de langage. | ||
| 54 | + | ||
| 55 | +La vue positionne donc un drapeau, et une horloge demande un redessin soixante fois par seconde tant que le drapeau est levé. Un réveil perdu ne peut rien bloquer, puisque le tic suivant est à seize millisecondes. | ||
| 56 | + | ||
| 57 | +## Windows : une pseudo-console, et pourquoi c'est un fichier à part | ||
| 58 | + | ||
| 59 | +Les pseudo-terminaux sont la seule partie non portable de tout ceci. Linux et macOS passent tous deux par `/dev/ptmx` et ne diffèrent que par l'`ioctl` qui accorde l'esclave. Windows n'a rien de tel : il a des **pseudo-consoles** — ConPTY, depuis Windows 10 version 1809 — un objet détenu par `conhost.exe` et relié à deux tubes de l'éditeur. Ce que le shell affiche arrive sur l'un des tubes sous la forme des mêmes séquences VT qu'un shell Unix écrit dans un pty, ce qui est la raison pour laquelle l'émulateur de ce côté n'a eu besoin d'aucun code Windows ; ce que l'éditeur écrit dans l'autre tube parvient au shell comme des frappes de touches. | ||
| 60 | + | ||
| 61 | +Trois choses en ont fait un fichier à part plutôt qu'une variante du fichier Unix. Le processus doit être créé à la main, parce que l'attacher à une pseudo-console exige un enregistrement de démarrage étendu que l'`os/exec` de Go ne sait pas porter. Le shell est `%COMSPEC%` — cmd.exe — plutôt que `$SHELL`, et cmd.exe lit sa ligne de commande selon ses propres règles : la ligne qui lance une commande du menu est donc composée pour lui mot pour mot, la commande entre une seule paire de guillemets, au lieu d'être échappée comme tout autre programme l'attend. Et `conhost.exe` garde le tube de sortie ouvert jusqu'à la fermeture de la console, quoi que fasse le shell ; une goroutine attend donc la fin du shell puis ferme la console — c'est ce qui transforme une commande terminée en la fin d'entrée sur laquelle la fenêtre compte pour le dire. Le contrôle de tâches est celui de cmd.exe et non du noyau : `Ctrl-C` interrompt le programme en cours comme il le ferait dans une fenêtre de console. | ||
| 62 | + | ||
| 63 | +Les fichiers par plateforme restent séparés pour que chaque plateforme ait une implémentation honnête derrière une petite interface, et qu'une plateforme qui n'a ni l'un ni l'autre — les BSD, aujourd'hui — reçoive `ErrUnsupported`, que `F8` le dise clairement, et que rien d'autre dans l'éditeur ne soit affecté. | ||
| 64 | + | ||
| 65 | +**Le chemin Windows a été compilé et vérifié, pas exécuté.** turbo-core est développé sous Linux et son auteur travaille sous macOS. Les parties pures — le bloc d'environnement, la ligne de commande que veut cmd.exe — sont testées unitairement sur toute plateforme, et les appels à l'API compilent et passent `go vet` sous `GOOS=windows` ; personne n'a encore appuyé sur `F8` sur une machine Windows. [Le guide](../how-to/use-a-terminal.md) dit quoi essayer en premier. | ||
| 66 | + | ||
| 67 | +## Liens avec le reste | ||
| 68 | + | ||
| 69 | +- La liste exacte de ce qui est implémenté : [référence des fenêtres terminal](../reference/terminal.md) | ||
| 70 | +- En utiliser une : [Lancer des commandes shell sans quitter l'éditeur](../how-to/use-a-terminal.md) | ||
| 71 | +- Où `terminal` se situe parmi les paquets, et pourquoi le graphe est orienté : [Architecture](architecture.md) | ||
| 72 | +- Le décompte de dépendances que cette page ne cesse d'invoquer : [Décisions de conception](design-decisions.md) | ||
added
docs/fr/how-to/ask-about-code.md +70 -0 | new file mode 100644 | ||
| @@ -0,0 +1,70 @@ | ||
| 1 | +# Comment interroger le code | |
| 2 | + | |
| 3 | +Ce guide montre comment suivre un nom dans un fichier Golo : ce que c'est, où il est déclaré, ce que déclare le fichier, et ce qui ne va pas. Il suppose Turbo Golo installé et un serveur de langage en marche — la barre d'état affiche `LSP: ready` quand c'est le cas. | |
| 4 | + | |
| 5 | +Pour se déplacer dans un fichier — chercher, aller à une ligne, changer de fenêtre — voir plutôt [Comment se déplacer dans un fichier](navigate-code.md). | |
| 6 | + | |
| 7 | +## Placez le curseur sur un nom | |
| 8 | + | |
| 9 | +N'importe lequel de ses caractères suffit. Chaque question ci-dessous porte sur la **position du curseur**, pas sur une sélection : il n'y a rien à surligner d'abord. | |
| 10 | + | |
| 11 | +## Demandez | |
| 12 | + | |
| 13 | +| Pour trouver | Faites | Raccourci | | |
| 14 | +| --- | --- | --- | | |
| 15 | +| Ce que c'est | **Code ▸ Describe symbol** | `F1` | | |
| 16 | +| Où c'est déclaré | **Code ▸ Go to definition** | `F12` | | |
| 17 | +| Où son *type* est déclaré | **Code ▸ Go to type definition** | | | |
| 18 | +| Ce qui l'implémente | **Code ▸ Find implementations…** | | | |
| 19 | +| Partout où c'est utilisé | **Code ▸ Find references…** | `Shift-F12` | | |
| 20 | + | |
| 21 | +**Une de ces cinq entrées ne signale rien avec `golo lsp`.** Le serveur — l'interpréteur lui-même, en mode serveur de langage — n'annonce pas `typeDefinition` : **Go to type definition** répond donc qu'il n'y a rien, quelle que soit la qualité du code. Les quatre autres fonctionnent, dans le fichier : **Find references** liste la déclaration et chaque appel, et **Find implementations** répond par la déclaration — Golo n'a pas d'interfaces, une fonction est sa propre implémentation. Les deux recherches de symbole ci-dessous fonctionnent aussi. Voir [coloration et complétion](../explanation/colouring-and-completion.md) pour savoir pourquoi cette seule lacune est écrite plutôt que cachée derrière une entrée de menu grisée. | |
| 22 | + | |
| 23 | +Ce que le serveur sait est **local au fichier** : la déclaration d'une fonction ou d'une union écrite au premier niveau du fichier devant vous, et les symboles apportés par un `import` d'un module de la bibliothèque standard embarquée — `gololang.Errors`, `gololang.Types`, `gololang.Ui`… Un `import` d'un module à vous, sur le disque, n'est pas résolu. | |
| 24 | + | |
| 25 | +**Describe symbol** sur une fonction que vous avez déclarée montre sa signature et les commentaires `#` écrits juste au-dessus d'elle : c'est là qu'il faut documenter. | |
| 26 | + | |
| 27 | +Une seule réponse vous y emmène directement. Plusieurs ouvrent une liste montrant le fichier, sa ligne, et le texte de cette ligne ; déplacez-vous aux flèches, `Entrée` pour y aller, `Échap` pour rester. | |
| 28 | + | |
| 29 | +## Quand rien ne revient | |
| 30 | + | |
| 31 | +Trois choses se ressemblent, et la barre d'état les distingue : | |
| 32 | + | |
| 33 | +| Elle affiche | Signification | | |
| 34 | +| --- | --- | | |
| 35 | +| `No references found`, ou l'équivalent pour la question posée | Le serveur a répondu, et il n'y en a pas — c'est la réponse permanente des trois questions que `golo lsp` ne sait pas traiter | | |
| 36 | +| Autre chose, par exemple `LSP: starting…` | Le serveur n'est pas encore prêt. Attendez un instant et redemandez. | | |
| 37 | +| `LSP: off` dans la barre d'état | Aucun serveur ne tourne. Voir [Comment activer la complétion](enable-completion.md). | | |
| 38 | + | |
| 39 | +La deuxième mérite d'être connue : un serveur qui démarre encore répond rien à toutes les questions, et c'est indiscernable d'une vraie réponse si l'éditeur ne le dit pas. | |
| 40 | + | |
| 41 | +## Chercher par le nom | |
| 42 | + | |
| 43 | +- **Code ▸ Symbol in file…** liste ce que déclare le fichier devant vous, indenté, avec la sorte de chaque symbole — un plan que l'on parcourt. Pour du Golo : les fonctions et les unions du premier niveau, les variantes d'une union rangées sous elle. | |
| 44 | +- **Code ▸ Symbol in project…** (`Ctrl-T`) demande un nom et cherche dans chaque fichier `.golo` sous la racine du projet, ouvert ou non — fonctions, unions et noms de module de premier niveau. Un nom vide les liste tous. | |
| 45 | + | |
| 46 | +## Voir ce qui ne va pas | |
| 47 | + | |
| 48 | +**Code ▸ Problems…** liste tous les problèmes signalés par le serveur, pour **tous les fichiers qu'il a chargés** — le plus souvent davantage que celui que vous éditez. En choisir un vous mène à la ligne. | |
| 49 | + | |
| 50 | +`golo lsp` publie ses diagnostics à l'ouverture d'un fichier et à chaque modification, sans qu'on lui demande. Ce sont les erreurs de syntaxe de son lexeur et de son analyseur, plus deux vérifications qui lui sont propres : la confusion entre `:` et `.`, et les commentaires à la C — `//` et `/* */` — là où Golo emploie `#` et `----`. Une erreur porte sur une **ligne entière** : le serveur donne un numéro de ligne quand son message en contient un, la ligne 1 sinon, et jamais de colonne. | |
| 51 | + | |
| 52 | +Les lignes à problème portent une marque dans la gouttière, à côté du numéro de ligne : | |
| 53 | + | |
| 54 | +| Marque | Signification | | |
| 55 | +| --- | --- | | |
| 56 | +| `×` | Une erreur | | |
| 57 | +| `!` | Un avertissement | | |
| 58 | +| `i` | Une information | | |
| 59 | +| `·` | Une suggestion | | |
| 60 | + | |
| 61 | +Une ligne qui a plusieurs problèmes montre le pire d'entre eux. | |
| 62 | + | |
| 63 | +**Les marques ont besoin des numéros de ligne.** Elles occupent la colonne qui sépare les numéros du texte : masquer la gouttière avec **Options ▸ Line numbers** les masque aussi. | |
| 64 | + | |
| 65 | +## Voir aussi | |
| 66 | + | |
| 67 | +- Chaque entrée et sa touche : [Menus](../reference/menus.md) | |
| 68 | +- Faire tourner un serveur : [Comment activer la complétion](enable-completion.md) | |
| 69 | +- Ce que l'éditeur demande, et pourquoi : [Coloration et complétion](../explanation/colouring-and-completion.md) | |
| 70 | +- Ce que `golo lsp` sait et ne sait pas répondre : [Outils Golo](../explanation/golo-tools.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | +# Comment interroger le code | ||
| 2 | + | ||
| 3 | +Ce guide montre comment suivre un nom dans un fichier Golo : ce que c'est, où il est déclaré, ce que déclare le fichier, et ce qui ne va pas. Il suppose Turbo Golo installé et un serveur de langage en marche — la barre d'état affiche `LSP: ready` quand c'est le cas. | ||
| 4 | + | ||
| 5 | +Pour se déplacer dans un fichier — chercher, aller à une ligne, changer de fenêtre — voir plutôt [Comment se déplacer dans un fichier](navigate-code.md). | ||
| 6 | + | ||
| 7 | +## Placez le curseur sur un nom | ||
| 8 | + | ||
| 9 | +N'importe lequel de ses caractères suffit. Chaque question ci-dessous porte sur la **position du curseur**, pas sur une sélection : il n'y a rien à surligner d'abord. | ||
| 10 | + | ||
| 11 | +## Demandez | ||
| 12 | + | ||
| 13 | +| Pour trouver | Faites | Raccourci | | ||
| 14 | +| --- | --- | --- | | ||
| 15 | +| Ce que c'est | **Code ▸ Describe symbol** | `F1` | | ||
| 16 | +| Où c'est déclaré | **Code ▸ Go to definition** | `F12` | | ||
| 17 | +| Où son *type* est déclaré | **Code ▸ Go to type definition** | | | ||
| 18 | +| Ce qui l'implémente | **Code ▸ Find implementations…** | | | ||
| 19 | +| Partout où c'est utilisé | **Code ▸ Find references…** | `Shift-F12` | | ||
| 20 | + | ||
| 21 | +**Une de ces cinq entrées ne signale rien avec `golo lsp`.** Le serveur — l'interpréteur lui-même, en mode serveur de langage — n'annonce pas `typeDefinition` : **Go to type definition** répond donc qu'il n'y a rien, quelle que soit la qualité du code. Les quatre autres fonctionnent, dans le fichier : **Find references** liste la déclaration et chaque appel, et **Find implementations** répond par la déclaration — Golo n'a pas d'interfaces, une fonction est sa propre implémentation. Les deux recherches de symbole ci-dessous fonctionnent aussi. Voir [coloration et complétion](../explanation/colouring-and-completion.md) pour savoir pourquoi cette seule lacune est écrite plutôt que cachée derrière une entrée de menu grisée. | ||
| 22 | + | ||
| 23 | +Ce que le serveur sait est **local au fichier** : la déclaration d'une fonction ou d'une union écrite au premier niveau du fichier devant vous, et les symboles apportés par un `import` d'un module de la bibliothèque standard embarquée — `gololang.Errors`, `gololang.Types`, `gololang.Ui`… Un `import` d'un module à vous, sur le disque, n'est pas résolu. | ||
| 24 | + | ||
| 25 | +**Describe symbol** sur une fonction que vous avez déclarée montre sa signature et les commentaires `#` écrits juste au-dessus d'elle : c'est là qu'il faut documenter. | ||
| 26 | + | ||
| 27 | +Une seule réponse vous y emmène directement. Plusieurs ouvrent une liste montrant le fichier, sa ligne, et le texte de cette ligne ; déplacez-vous aux flèches, `Entrée` pour y aller, `Échap` pour rester. | ||
| 28 | + | ||
| 29 | +## Quand rien ne revient | ||
| 30 | + | ||
| 31 | +Trois choses se ressemblent, et la barre d'état les distingue : | ||
| 32 | + | ||
| 33 | +| Elle affiche | Signification | | ||
| 34 | +| --- | --- | | ||
| 35 | +| `No references found`, ou l'équivalent pour la question posée | Le serveur a répondu, et il n'y en a pas — c'est la réponse permanente des trois questions que `golo lsp` ne sait pas traiter | | ||
| 36 | +| Autre chose, par exemple `LSP: starting…` | Le serveur n'est pas encore prêt. Attendez un instant et redemandez. | | ||
| 37 | +| `LSP: off` dans la barre d'état | Aucun serveur ne tourne. Voir [Comment activer la complétion](enable-completion.md). | | ||
| 38 | + | ||
| 39 | +La deuxième mérite d'être connue : un serveur qui démarre encore répond rien à toutes les questions, et c'est indiscernable d'une vraie réponse si l'éditeur ne le dit pas. | ||
| 40 | + | ||
| 41 | +## Chercher par le nom | ||
| 42 | + | ||
| 43 | +- **Code ▸ Symbol in file…** liste ce que déclare le fichier devant vous, indenté, avec la sorte de chaque symbole — un plan que l'on parcourt. Pour du Golo : les fonctions et les unions du premier niveau, les variantes d'une union rangées sous elle. | ||
| 44 | +- **Code ▸ Symbol in project…** (`Ctrl-T`) demande un nom et cherche dans chaque fichier `.golo` sous la racine du projet, ouvert ou non — fonctions, unions et noms de module de premier niveau. Un nom vide les liste tous. | ||
| 45 | + | ||
| 46 | +## Voir ce qui ne va pas | ||
| 47 | + | ||
| 48 | +**Code ▸ Problems…** liste tous les problèmes signalés par le serveur, pour **tous les fichiers qu'il a chargés** — le plus souvent davantage que celui que vous éditez. En choisir un vous mène à la ligne. | ||
| 49 | + | ||
| 50 | +`golo lsp` publie ses diagnostics à l'ouverture d'un fichier et à chaque modification, sans qu'on lui demande. Ce sont les erreurs de syntaxe de son lexeur et de son analyseur, plus deux vérifications qui lui sont propres : la confusion entre `:` et `.`, et les commentaires à la C — `//` et `/* */` — là où Golo emploie `#` et `----`. Une erreur porte sur une **ligne entière** : le serveur donne un numéro de ligne quand son message en contient un, la ligne 1 sinon, et jamais de colonne. | ||
| 51 | + | ||
| 52 | +Les lignes à problème portent une marque dans la gouttière, à côté du numéro de ligne : | ||
| 53 | + | ||
| 54 | +| Marque | Signification | | ||
| 55 | +| --- | --- | | ||
| 56 | +| `×` | Une erreur | | ||
| 57 | +| `!` | Un avertissement | | ||
| 58 | +| `i` | Une information | | ||
| 59 | +| `·` | Une suggestion | | ||
| 60 | + | ||
| 61 | +Une ligne qui a plusieurs problèmes montre le pire d'entre eux. | ||
| 62 | + | ||
| 63 | +**Les marques ont besoin des numéros de ligne.** Elles occupent la colonne qui sépare les numéros du texte : masquer la gouttière avec **Options ▸ Line numbers** les masque aussi. | ||
| 64 | + | ||
| 65 | +## Voir aussi | ||
| 66 | + | ||
| 67 | +- Chaque entrée et sa touche : [Menus](../reference/menus.md) | ||
| 68 | +- Faire tourner un serveur : [Comment activer la complétion](enable-completion.md) | ||
| 69 | +- Ce que l'éditeur demande, et pourquoi : [Coloration et complétion](../explanation/colouring-and-completion.md) | ||
| 70 | +- Ce que `golo lsp` sait et ne sait pas répondre : [Outils Golo](../explanation/golo-tools.md) | ||
added
docs/fr/how-to/browse-a-project.md +69 -0 | new file mode 100644 | ||
| @@ -0,0 +1,69 @@ | ||
| 1 | +# Parcourir un projet et ouvrir des fichiers depuis un arbre | |
| 2 | + | |
| 3 | +Ce guide montre comment ouvrir l'arbre du projet, le parcourir et y ouvrir un fichier. Il suppose Turbo Golo déjà installé. | |
| 4 | + | |
| 5 | +## Ouvrir l'arbre | |
| 6 | + | |
| 7 | +Lancez l'éditeur **depuis le dossier du projet**, puis appuyez sur `F9`, ou choisissez **Window ▸ Project tree**. | |
| 8 | + | |
| 9 | +Une fenêtre s'ouvre avec les fichiers du projet, nommée d'après le dossier depuis lequel l'éditeur a été lancé : | |
| 10 | + | |
| 11 | +``` | |
| 12 | +╔═[x]═══════════════ shapes ═══════════════2═[■]╗ | |
| 13 | +║ ▶ .turbo-golo ║ | |
| 14 | +║ ▼ lib ║ | |
| 15 | +║ geometry.golo ║ | |
| 16 | +║ .gitignore ║ | |
| 17 | +║ README.md ║ | |
| 18 | +║ shapes.golo ║ | |
| 19 | +║ shapes_test.golo ║ | |
| 20 | +╚═══════════════════════════════════════════════╝ | |
| 21 | +``` | |
| 22 | + | |
| 23 | +Les dossiers viennent d'abord, puis les fichiers, chaque groupe trié. `.git` est la seule chose masquée — `.turbo-golo`, `.gitignore` et les autres sont des fichiers de votre projet, que vous voudrez sans doute ouvrir. | |
| 24 | + | |
| 25 | +Appuyer de nouveau sur `F9` ramène cette fenêtre au premier plan plutôt que d'ouvrir un second arbre. | |
| 26 | + | |
| 27 | +## Le parcourir | |
| 28 | + | |
| 29 | +| Touche | Effet | | |
| 30 | +| --- | --- | | |
| 31 | +| `↑` `↓` | Déplacer la surbrillance | | |
| 32 | +| `→` | Ouvrir un dossier fermé ; sur autre chose, passer à la ligne suivante | | |
| 33 | +| `←` | Fermer un dossier ouvert ; sur autre chose, remonter au dossier qui le contient | | |
| 34 | +| `Entrée` | Ouvrir un fichier, ou ouvrir et fermer un dossier | | |
| 35 | +| `Début` `Fin` | Première / dernière ligne | | |
| 36 | +| `Page↑` `Page↓` | Un écran à la fois | | |
| 37 | + | |
| 38 | +Un dossier est lu la première fois que vous l'ouvrez : un arbre sur un gros projet coûte donc une lecture de dossier, pas un parcours complet. | |
| 39 | + | |
| 40 | +## Ouvrir un fichier | |
| 41 | + | |
| 42 | +Placez la surbrillance dessus et appuyez sur `Entrée`, ou cliquez-le deux fois. | |
| 43 | + | |
| 44 | +Le fichier s'ouvre dans une fenêtre à lui, devant l'arbre. Un fichier déjà ouvert est ramené au premier plan plutôt qu'ouvert deux fois. | |
| 45 | + | |
| 46 | +## Voir un fichier créé après l'ouverture de l'arbre | |
| 47 | + | |
| 48 | +L'arbre ne surveille pas le disque. Appuyez sur **`F5`** ou **`Ctrl-R`** avec l'arbre au premier plan : il relit le projet, en gardant ouvert ce que vous aviez ouvert et la surbrillance sur la même entrée. | |
| 49 | + | |
| 50 | +Enregistrer un fichier rafraîchit l'arbre pour vous : un **File ▸ Save as** sous un nouveau nom y apparaît sans rien demander. Un fichier créé autrement — un `golo new` ou un `gogolo build` dans une fenêtre terminal, ou un `git checkout` — demande la touche de rafraîchissement. | |
| 51 | + | |
| 52 | +## Travailler avec l'arbre et un fichier côte à côte | |
| 53 | + | |
| 54 | +L'arbre est une fenêtre ordinaire, donc toutes les commandes de fenêtre s'y appliquent : | |
| 55 | + | |
| 56 | +- **Window ▸ Tile** place l'arbre et votre fichier côte à côte. | |
| 57 | +- Tirez son coin inférieur droit pour le rétrécir une fois que vous vous y retrouvez. | |
| 58 | +- `[x]` le ferme ; `F9` le ramène. | |
| 59 | + | |
| 60 | +## Variantes | |
| 61 | + | |
| 62 | +- **Vous avez lancé l'éditeur depuis un sous-dossier.** L'arbre y est enraciné et ne montre que cette partie du projet. Lancez plutôt depuis le dossier du projet — la même règle que `.turbo-golo/settings.toml`. | |
| 63 | +- **Un dossier apparaît ouvert mais vide.** Il n'a pas pu être lu, le plus souvent un problème de permissions. Le reste de l'arbre n'est pas affecté ; corrigez les permissions et appuyez sur `F5`. | |
| 64 | + | |
| 65 | +## Voir aussi | |
| 66 | + | |
| 67 | +- Toutes les touches et tout ce que l'arbre montre, exactement : [Référence de l'arbre du projet](../reference/project-tree.md) | |
| 68 | +- Pourquoi c'est une fenêtre et non un panneau ancré, et pourquoi il ne surveille pas le disque : [Arbre du projet](../explanation/project-tree.md) | |
| 69 | +- Le colorer : [Format des fichiers de thème](../reference/themes.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,69 @@ | |||
| 1 | +# Parcourir un projet et ouvrir des fichiers depuis un arbre | ||
| 2 | + | ||
| 3 | +Ce guide montre comment ouvrir l'arbre du projet, le parcourir et y ouvrir un fichier. Il suppose Turbo Golo déjà installé. | ||
| 4 | + | ||
| 5 | +## Ouvrir l'arbre | ||
| 6 | + | ||
| 7 | +Lancez l'éditeur **depuis le dossier du projet**, puis appuyez sur `F9`, ou choisissez **Window ▸ Project tree**. | ||
| 8 | + | ||
| 9 | +Une fenêtre s'ouvre avec les fichiers du projet, nommée d'après le dossier depuis lequel l'éditeur a été lancé : | ||
| 10 | + | ||
| 11 | +``` | ||
| 12 | +╔═[x]═══════════════ shapes ═══════════════2═[■]╗ | ||
| 13 | +║ ▶ .turbo-golo ║ | ||
| 14 | +║ ▼ lib ║ | ||
| 15 | +║ geometry.golo ║ | ||
| 16 | +║ .gitignore ║ | ||
| 17 | +║ README.md ║ | ||
| 18 | +║ shapes.golo ║ | ||
| 19 | +║ shapes_test.golo ║ | ||
| 20 | +╚═══════════════════════════════════════════════╝ | ||
| 21 | +``` | ||
| 22 | + | ||
| 23 | +Les dossiers viennent d'abord, puis les fichiers, chaque groupe trié. `.git` est la seule chose masquée — `.turbo-golo`, `.gitignore` et les autres sont des fichiers de votre projet, que vous voudrez sans doute ouvrir. | ||
| 24 | + | ||
| 25 | +Appuyer de nouveau sur `F9` ramène cette fenêtre au premier plan plutôt que d'ouvrir un second arbre. | ||
| 26 | + | ||
| 27 | +## Le parcourir | ||
| 28 | + | ||
| 29 | +| Touche | Effet | | ||
| 30 | +| --- | --- | | ||
| 31 | +| `↑` `↓` | Déplacer la surbrillance | | ||
| 32 | +| `→` | Ouvrir un dossier fermé ; sur autre chose, passer à la ligne suivante | | ||
| 33 | +| `←` | Fermer un dossier ouvert ; sur autre chose, remonter au dossier qui le contient | | ||
| 34 | +| `Entrée` | Ouvrir un fichier, ou ouvrir et fermer un dossier | | ||
| 35 | +| `Début` `Fin` | Première / dernière ligne | | ||
| 36 | +| `Page↑` `Page↓` | Un écran à la fois | | ||
| 37 | + | ||
| 38 | +Un dossier est lu la première fois que vous l'ouvrez : un arbre sur un gros projet coûte donc une lecture de dossier, pas un parcours complet. | ||
| 39 | + | ||
| 40 | +## Ouvrir un fichier | ||
| 41 | + | ||
| 42 | +Placez la surbrillance dessus et appuyez sur `Entrée`, ou cliquez-le deux fois. | ||
| 43 | + | ||
| 44 | +Le fichier s'ouvre dans une fenêtre à lui, devant l'arbre. Un fichier déjà ouvert est ramené au premier plan plutôt qu'ouvert deux fois. | ||
| 45 | + | ||
| 46 | +## Voir un fichier créé après l'ouverture de l'arbre | ||
| 47 | + | ||
| 48 | +L'arbre ne surveille pas le disque. Appuyez sur **`F5`** ou **`Ctrl-R`** avec l'arbre au premier plan : il relit le projet, en gardant ouvert ce que vous aviez ouvert et la surbrillance sur la même entrée. | ||
| 49 | + | ||
| 50 | +Enregistrer un fichier rafraîchit l'arbre pour vous : un **File ▸ Save as** sous un nouveau nom y apparaît sans rien demander. Un fichier créé autrement — un `golo new` ou un `gogolo build` dans une fenêtre terminal, ou un `git checkout` — demande la touche de rafraîchissement. | ||
| 51 | + | ||
| 52 | +## Travailler avec l'arbre et un fichier côte à côte | ||
| 53 | + | ||
| 54 | +L'arbre est une fenêtre ordinaire, donc toutes les commandes de fenêtre s'y appliquent : | ||
| 55 | + | ||
| 56 | +- **Window ▸ Tile** place l'arbre et votre fichier côte à côte. | ||
| 57 | +- Tirez son coin inférieur droit pour le rétrécir une fois que vous vous y retrouvez. | ||
| 58 | +- `[x]` le ferme ; `F9` le ramène. | ||
| 59 | + | ||
| 60 | +## Variantes | ||
| 61 | + | ||
| 62 | +- **Vous avez lancé l'éditeur depuis un sous-dossier.** L'arbre y est enraciné et ne montre que cette partie du projet. Lancez plutôt depuis le dossier du projet — la même règle que `.turbo-golo/settings.toml`. | ||
| 63 | +- **Un dossier apparaît ouvert mais vide.** Il n'a pas pu être lu, le plus souvent un problème de permissions. Le reste de l'arbre n'est pas affecté ; corrigez les permissions et appuyez sur `F5`. | ||
| 64 | + | ||
| 65 | +## Voir aussi | ||
| 66 | + | ||
| 67 | +- Toutes les touches et tout ce que l'arbre montre, exactement : [Référence de l'arbre du projet](../reference/project-tree.md) | ||
| 68 | +- Pourquoi c'est une fenêtre et non un panneau ancré, et pourquoi il ne surveille pas le disque : [Arbre du projet](../explanation/project-tree.md) | ||
| 69 | +- Le colorer : [Format des fichiers de thème](../reference/themes.md) | ||
added
docs/fr/how-to/configure-a-project.md +83 -0 | new file mode 100644 | ||
| @@ -0,0 +1,83 @@ | ||
| 1 | +# Donner ses propres réglages à un projet | |
| 2 | + | |
| 3 | +Ce guide montre comment fixer un thème et activer la sauvegarde automatique pour un projet, afin que tous ceux qui l'ouvrent aient le même éditeur. Il suppose Turbo Golo déjà installé. | |
| 4 | + | |
| 5 | +## Créer le fichier de réglages | |
| 6 | + | |
| 7 | +Lancez l'éditeur **depuis le dossier du projet**, puis choisissez **Options ▸ Create project settings**. | |
| 8 | + | |
| 9 | +Cela écrit `.turbo-golo/settings.toml`, rempli avec le thème que vous utilisez à cet instant, et l'ouvre pour édition — coloré, puisque Turbo Golo colore le TOML : | |
| 10 | + | |
| 11 | +```toml | |
| 12 | +# turbo-golo project settings. | |
| 13 | +# | |
| 14 | +# These apply to everyone who opens this project in turbo-golo. Delete this | |
| 15 | +# file and the editor falls back to its own defaults. | |
| 16 | + | |
| 17 | +[editor] | |
| 18 | + | |
| 19 | +# The colour theme to start in. `turbo-golo -list-themes` lists them all. | |
| 20 | +# A -theme flag on the command line overrides this. | |
| 21 | +theme = "turbo-classic" | |
| 22 | + | |
| 23 | +# Write modified files by themselves, a short while after you stop typing. | |
| 24 | +# On, because a project that has gone to the trouble of having a settings file | |
| 25 | +# has said what it wants; set it to false and save, and it stops at once. | |
| 26 | +autosave = true | |
| 27 | + | |
| 28 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | |
| 29 | +autosave_delay = "2s" | |
| 30 | +``` | |
| 31 | + | |
| 32 | +Le fichier est lu au démarrage de l'éditeur, et **de nouveau à chaque fois que vous l'enregistrez** — une modification est donc en vigueur dès que vous appuyez sur `F2`. La barre d'état le confirme : `Applied .turbo-golo/settings.toml — autosave on (2s)`. | |
| 33 | + | |
| 34 | +Cela vaut pour les réglages que ce fichier contient, pas pour le thème : **Options ▸ Theme** est la façon vivante de le changer, et y réécrit votre choix pour vous. | |
| 35 | + | |
| 36 | +L'entrée de menu que vous venez d'utiliser est maintenant grisée, et **Options ▸ Project settings…** à côté d'elle ne l'est plus. C'est la règle pour les trois fichiers du projet : vous pouvez créer celui que vous n'avez pas, et ouvrir celui que vous avez. | |
| 37 | + | |
| 38 | +## La sauvegarde automatique | |
| 39 | + | |
| 40 | +Elle est déjà active : le fichier qu'on vient de vous donner dit `autosave = true`. | |
| 41 | + | |
| 42 | +Tout fichier qui a un nom est écrit deux secondes après que vous ayez cessé de taper. La barre d'état affiche `Saved main.golo` au moment où cela arrive. Rien n'est écrit tant que vous tapez : chaque frappe repousse l'attente. | |
| 43 | + | |
| 44 | +Deux choses changent en conséquence, toutes deux voulues : | |
| 45 | + | |
| 46 | +- **Fermer une fenêtre ne demande plus** s'il faut enregistrer. Le fichier allait l'être de toute façon. | |
| 47 | +- **Quitter l'éditeur ne demande plus** non plus, pour la même raison. | |
| 48 | + | |
| 49 | +Un fichier qui n'a jamais reçu de nom fait exception : la sauvegarde automatique n'ouvre jamais de dialogue, donc une fenêtre sans titre garde son `*` et la question est toujours posée à la fermeture. | |
| 50 | + | |
| 51 | +Pour la désactiver, mettez `autosave` à `false` et enregistrez ; la barre d'état répond `autosave off`, et cela s'arrête à l'instant. Pour attendre plus ou moins longtemps, changez `autosave_delay` : | |
| 52 | + | |
| 53 | +```toml | |
| 54 | +autosave_delay = "500ms" | |
| 55 | +``` | |
| 56 | + | |
| 57 | +## Fixer le thème | |
| 58 | + | |
| 59 | +Donnez à `theme` un nom parmi ceux de `turbo-golo -list-themes`, ou choisissez-en un simplement avec **Options ▸ Theme** : dès lors qu'un fichier de réglages existe, choisir un thème l'y écrit pour vous, en conservant vos commentaires et votre mise en page. | |
| 60 | + | |
| 61 | +## Essayer un autre thème sans toucher au fichier | |
| 62 | + | |
| 63 | +Passez `-theme` sur la ligne de commande. Il l'emporte sur le choix du projet, le temps de cette exécution seulement : | |
| 64 | + | |
| 65 | +```bash | |
| 66 | +turbo-golo -theme turbo-dark main.golo | |
| 67 | +``` | |
| 68 | + | |
| 69 | +## Modifier le fichier plus tard | |
| 70 | + | |
| 71 | +**Options ▸ Project settings…** le rouvre. L'entrée est grisée dans un projet qui n'en a pas. | |
| 72 | + | |
| 73 | +## Variantes | |
| 74 | + | |
| 75 | +- **Vous lancez l'éditeur depuis un sous-dossier.** Les réglages ne sont pas trouvés : seul `./.turbo-golo` est consulté, sans remontée vers la racine du projet. Lancez depuis le dossier du projet, ou passez `-theme` pour cette fois. | |
| 76 | +- **Le fichier contient une erreur.** L'éditeur le signale sur la sortie d'erreur et s'ouvre avec ses valeurs par défaut — vous pouvez donc corriger le fichier dans l'éditeur lui-même. | |
| 77 | +- **Vous partagez le projet.** `.turbo-golo/settings.toml` est un fichier ordinaire : versionnez-le pour convenir d'un thème en équipe, ou ajoutez-le à `.gitignore` pour le garder pour vous. | |
| 78 | + | |
| 79 | +## Voir aussi | |
| 80 | + | |
| 81 | +- Chaque clé, avec son type et sa valeur par défaut : [Référence des réglages de projet](../reference/project-settings.md) | |
| 82 | +- Pourquoi le fichier n'est pas cherché dans les dossiers parents, et pourquoi l'autosave attend : [Réglages de projet](../explanation/project-settings.md) | |
| 83 | +- Écrire un thème à fixer : [Écrire son propre thème](write-a-theme.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | +# Donner ses propres réglages à un projet | ||
| 2 | + | ||
| 3 | +Ce guide montre comment fixer un thème et activer la sauvegarde automatique pour un projet, afin que tous ceux qui l'ouvrent aient le même éditeur. Il suppose Turbo Golo déjà installé. | ||
| 4 | + | ||
| 5 | +## Créer le fichier de réglages | ||
| 6 | + | ||
| 7 | +Lancez l'éditeur **depuis le dossier du projet**, puis choisissez **Options ▸ Create project settings**. | ||
| 8 | + | ||
| 9 | +Cela écrit `.turbo-golo/settings.toml`, rempli avec le thème que vous utilisez à cet instant, et l'ouvre pour édition — coloré, puisque Turbo Golo colore le TOML : | ||
| 10 | + | ||
| 11 | +```toml | ||
| 12 | +# turbo-golo project settings. | ||
| 13 | +# | ||
| 14 | +# These apply to everyone who opens this project in turbo-golo. Delete this | ||
| 15 | +# file and the editor falls back to its own defaults. | ||
| 16 | + | ||
| 17 | +[editor] | ||
| 18 | + | ||
| 19 | +# The colour theme to start in. `turbo-golo -list-themes` lists them all. | ||
| 20 | +# A -theme flag on the command line overrides this. | ||
| 21 | +theme = "turbo-classic" | ||
| 22 | + | ||
| 23 | +# Write modified files by themselves, a short while after you stop typing. | ||
| 24 | +# On, because a project that has gone to the trouble of having a settings file | ||
| 25 | +# has said what it wants; set it to false and save, and it stops at once. | ||
| 26 | +autosave = true | ||
| 27 | + | ||
| 28 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | ||
| 29 | +autosave_delay = "2s" | ||
| 30 | +``` | ||
| 31 | + | ||
| 32 | +Le fichier est lu au démarrage de l'éditeur, et **de nouveau à chaque fois que vous l'enregistrez** — une modification est donc en vigueur dès que vous appuyez sur `F2`. La barre d'état le confirme : `Applied .turbo-golo/settings.toml — autosave on (2s)`. | ||
| 33 | + | ||
| 34 | +Cela vaut pour les réglages que ce fichier contient, pas pour le thème : **Options ▸ Theme** est la façon vivante de le changer, et y réécrit votre choix pour vous. | ||
| 35 | + | ||
| 36 | +L'entrée de menu que vous venez d'utiliser est maintenant grisée, et **Options ▸ Project settings…** à côté d'elle ne l'est plus. C'est la règle pour les trois fichiers du projet : vous pouvez créer celui que vous n'avez pas, et ouvrir celui que vous avez. | ||
| 37 | + | ||
| 38 | +## La sauvegarde automatique | ||
| 39 | + | ||
| 40 | +Elle est déjà active : le fichier qu'on vient de vous donner dit `autosave = true`. | ||
| 41 | + | ||
| 42 | +Tout fichier qui a un nom est écrit deux secondes après que vous ayez cessé de taper. La barre d'état affiche `Saved main.golo` au moment où cela arrive. Rien n'est écrit tant que vous tapez : chaque frappe repousse l'attente. | ||
| 43 | + | ||
| 44 | +Deux choses changent en conséquence, toutes deux voulues : | ||
| 45 | + | ||
| 46 | +- **Fermer une fenêtre ne demande plus** s'il faut enregistrer. Le fichier allait l'être de toute façon. | ||
| 47 | +- **Quitter l'éditeur ne demande plus** non plus, pour la même raison. | ||
| 48 | + | ||
| 49 | +Un fichier qui n'a jamais reçu de nom fait exception : la sauvegarde automatique n'ouvre jamais de dialogue, donc une fenêtre sans titre garde son `*` et la question est toujours posée à la fermeture. | ||
| 50 | + | ||
| 51 | +Pour la désactiver, mettez `autosave` à `false` et enregistrez ; la barre d'état répond `autosave off`, et cela s'arrête à l'instant. Pour attendre plus ou moins longtemps, changez `autosave_delay` : | ||
| 52 | + | ||
| 53 | +```toml | ||
| 54 | +autosave_delay = "500ms" | ||
| 55 | +``` | ||
| 56 | + | ||
| 57 | +## Fixer le thème | ||
| 58 | + | ||
| 59 | +Donnez à `theme` un nom parmi ceux de `turbo-golo -list-themes`, ou choisissez-en un simplement avec **Options ▸ Theme** : dès lors qu'un fichier de réglages existe, choisir un thème l'y écrit pour vous, en conservant vos commentaires et votre mise en page. | ||
| 60 | + | ||
| 61 | +## Essayer un autre thème sans toucher au fichier | ||
| 62 | + | ||
| 63 | +Passez `-theme` sur la ligne de commande. Il l'emporte sur le choix du projet, le temps de cette exécution seulement : | ||
| 64 | + | ||
| 65 | +```bash | ||
| 66 | +turbo-golo -theme turbo-dark main.golo | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +## Modifier le fichier plus tard | ||
| 70 | + | ||
| 71 | +**Options ▸ Project settings…** le rouvre. L'entrée est grisée dans un projet qui n'en a pas. | ||
| 72 | + | ||
| 73 | +## Variantes | ||
| 74 | + | ||
| 75 | +- **Vous lancez l'éditeur depuis un sous-dossier.** Les réglages ne sont pas trouvés : seul `./.turbo-golo` est consulté, sans remontée vers la racine du projet. Lancez depuis le dossier du projet, ou passez `-theme` pour cette fois. | ||
| 76 | +- **Le fichier contient une erreur.** L'éditeur le signale sur la sortie d'erreur et s'ouvre avec ses valeurs par défaut — vous pouvez donc corriger le fichier dans l'éditeur lui-même. | ||
| 77 | +- **Vous partagez le projet.** `.turbo-golo/settings.toml` est un fichier ordinaire : versionnez-le pour convenir d'un thème en équipe, ou ajoutez-le à `.gitignore` pour le garder pour vous. | ||
| 78 | + | ||
| 79 | +## Voir aussi | ||
| 80 | + | ||
| 81 | +- Chaque clé, avec son type et sa valeur par défaut : [Référence des réglages de projet](../reference/project-settings.md) | ||
| 82 | +- Pourquoi le fichier n'est pas cherché dans les dossiers parents, et pourquoi l'autosave attend : [Réglages de projet](../explanation/project-settings.md) | ||
| 83 | +- Écrire un thème à fixer : [Écrire son propre thème](write-a-theme.md) | ||
added
docs/fr/how-to/enable-completion.md +137 -0 | new file mode 100644 | ||
| @@ -0,0 +1,137 @@ | ||
| 1 | +# Activer la complétion Golo | |
| 2 | + | |
| 3 | +Ce guide montre comment obtenir la complétion, les survols, l'aller-à-la-définition et les marques d'erreur. Il suppose que Turbo Golo est déjà installé. | |
| 4 | + | |
| 5 | +La complétion vient de **`golo lsp`** — l'interpréteur GoloScript lui-même, démarré en mode serveur de langage. Il n'y a pas de serveur séparé à installer : une machine qui peut exécuter un script Golo peut en compléter un. Turbo Golo n'embarque pas l'interpréteur pour autant : l'édition et la coloration marchent sans lui, et seules la complétion et les marques d'erreur manquent. | |
| 6 | + | |
| 7 | +## 1. Installer golo | |
| 8 | + | |
| 9 | +Soit un binaire depuis la [page des releases](https://codeberg.org/TypeUnsafe/golo-script/releases) : | |
| 10 | + | |
| 11 | +```bash | |
| 12 | +chmod +x golo-<version>-<plateforme> | |
| 13 | +sudo mv golo-<version>-<plateforme> /usr/local/bin/golo | |
| 14 | +golo --version | |
| 15 | +``` | |
| 16 | + | |
| 17 | +soit une construction depuis les sources, qui donne aussi les deux compilateurs : | |
| 18 | + | |
| 19 | +```bash | |
| 20 | +git clone https://codeberg.org/TypeUnsafe/golo-script.git && cd golo-script | |
| 21 | +./install.sh | |
| 22 | +``` | |
| 23 | + | |
| 24 | +L'installeur de Turbo Golo fera la seconde pour vous : `scripts/install.sh --with-server`. [Installer GoloScript](install-goloscript.md) donne le détail. | |
| 25 | + | |
| 26 | +## 2. S'assurer que Turbo Golo le trouve | |
| 27 | + | |
| 28 | +Turbo Golo regarde d'abord sur le `PATH`, puis dans `/usr/local/bin`, où écrit l'installeur de GoloScript. Vérifiez : | |
| 29 | + | |
| 30 | +```bash | |
| 31 | +golo --version | |
| 32 | +``` | |
| 33 | + | |
| 34 | +``` | |
| 35 | +v0.1.1 | dev.20260802.🤓 | |
| 36 | +``` | |
| 37 | + | |
| 38 | +Si cela dit « command not found » mais que Turbo Golo le trouve quand même, c'est attendu et sans conséquence : l'éditeur a cherché dans `/usr/local/bin` lui-même. | |
| 39 | + | |
| 40 | +## 3. Ouvrir un script | |
| 41 | + | |
| 42 | +```bash | |
| 43 | +cd /chemin/vers/vos/scripts | |
| 44 | +turbo-golo main.golo | |
| 45 | +``` | |
| 46 | + | |
| 47 | +Golo n'a pas de manifeste de projet, il n'y a donc rien à chercher : `golo lsp` est démarré dans le répertoire du fichier que vous avez ouvert, et il répond à propos de ce fichier. L'endroit d'où vous lancez l'éditeur ne change rien à la complétion — il décide où s'exécutent les commandes du menu Golo, ce qui est une autre affaire. | |
| 48 | + | |
| 49 | +## 4. Demander une complétion | |
| 50 | + | |
| 51 | +Tapez les premières lettres d'un nom et appuyez sur **Ctrl-Espace** : | |
| 52 | + | |
| 53 | +```golo | |
| 54 | +prin | |
| 55 | +``` | |
| 56 | + | |
| 57 | +Une liste se déroule sous le curseur — `print`, `println`, avec leurs signatures. Continuez à taper pour la réduire, **↑ ↓** pour la parcourir, **Entrée** ou **Tab** pour accepter, **Échap** pour l'écarter. | |
| 58 | + | |
| 59 | +## Ce que contient la liste | |
| 60 | + | |
| 61 | +`golo lsp` propose quatre sortes de choses : | |
| 62 | + | |
| 63 | +| Proposé | Exemple | | |
| 64 | +| --- | --- | | |
| 65 | +| Les mots-clés | `function`, `foreach`, `augment` | | |
| 66 | +| Les builtins de l'interpréteur, avec leurs signatures et leur documentation | `println`, `readFile`, `httpGet`, `DynamicObject` | | |
| 67 | +| Les fonctions et unions déclarées **au premier niveau** du fichier | votre propre `function helper = …` | | |
| 68 | +| Les symboles apportés par `import` depuis les modules embarqués dans le binaire | `Some`, `None`, `isSome`, `either` après `import gololang.Errors` | | |
| 69 | + | |
| 70 | +Deux choses n'y sont délibérément **pas**, et toutes deux ressemblent à un serveur cassé quand on ne le sait pas : | |
| 71 | + | |
| 72 | +- **Une fonction déclarée dans une autre fonction.** Seules les déclarations de premier niveau sont collectées. Sortez-la, ou acceptez qu'elle ne soit pas proposée. | |
| 73 | +- **Tout ce qui vient d'un fichier `.golo` à vous.** `import` résout les modules intégrés à l'interpréteur — `gololang.Errors`, `gololang.Types`, `gololang.Ui`, `gololang.Testing`, … — et rien sur le disque. Une fonction dans `lib/util.golo` n'est pas vue depuis `main.golo`. | |
| 74 | + | |
| 75 | +## Vérifier ce que fait le serveur | |
| 76 | + | |
| 77 | +L'extrémité droite de la barre d'état montre l'état du serveur de langage : `LSP: starting…`, `LSP: ready`, ou la raison pour laquelle il n'y en a pas : | |
| 78 | + | |
| 79 | +``` | |
| 80 | +LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases | |
| 81 | +``` | |
| 82 | + | |
| 83 | +`Run ▸ Language server status` montre la même chose dans une boîte, avec le chemin où le binaire a été trouvé et le répertoire où il a été démarré. | |
| 84 | + | |
| 85 | +## Variantes | |
| 86 | + | |
| 87 | +**Vous ne voulez aucun serveur de langage :** | |
| 88 | + | |
| 89 | +```bash | |
| 90 | +turbo-golo -no-lsp main.golo | |
| 91 | +``` | |
| 92 | + | |
| 93 | +**La complétion est morte dans une fenêtre née sans nom.** Une fenêtre « Untitled » n'a aucun fichier à annoncer à `golo lsp` tant qu'elle n'est pas enregistrée — appuyez sur **F2** et donnez-lui un nom en `.golo`. Dès cette sauvegarde, la complétion, le survol et les marques d'erreur fonctionnent dans cette fenêtre ; inutile de quitter et relancer l'éditeur. | |
| 94 | + | |
| 95 | +**La liste est vide.** `golo lsp` répond pour n'importe quel fichier, y compris un qui ne parse pas — il liste les mots-clés et les builtins quoi qu'il arrive — une liste vide signifie donc presque toujours que le serveur ne tourne pas. Lisez la barre d'état. | |
| 96 | + | |
| 97 | +**Ctrl-Espace ne fait rien.** tmux, screen et les terminaux des IDE s'approprient fréquemment `Ctrl-Espace` avant que l'éditeur le voie. Utilisez `Run ▸ Completion` à la place. | |
| 98 | + | |
| 99 | +**Une requête prend trop longtemps.** Chaque requête abandonne après quelques secondes, un serveur bloqué ralentit donc l'éditeur mais ne le fige jamais. La barre d'état signale l'échec. | |
| 100 | + | |
| 101 | +**Vous avez installé golo à un endroit inhabituel.** L'éditeur cherche sur le `PATH` et dans `/usr/local/bin`, et nulle part ailleurs — aucune variable d'environnement ne nomme un autre répertoire. Mettez le répertoire sur le `PATH`, ou un lien symbolique dans `/usr/local/bin`. | |
| 102 | + | |
| 103 | +## Ce que le serveur donne d'autre | |
| 104 | + | |
| 105 | +La complétion est la chose la plus bruyante qu'il fait et la moindre de ce qu'il sait. La même connexion répond à quatre autres questions, toutes dans le menu **Code** — trois à propos du symbole sous le curseur, aucune sélection nécessaire, et une à propos d'un nom que vous tapez. | |
| 106 | + | |
| 107 | +| Touche | Ce qu'elle fait | Avec `golo lsp` | | |
| 108 | +| --- | --- | --- | | |
| 109 | +| **Ctrl-Espace** | La liste de complétion | oui | | |
| 110 | +| **F1** | Décrire le symbole sous le curseur | oui — pour une fonction que vous avez déclarée, les commentaires `#` écrits juste au-dessus ; pour un builtin, sa signature et un exemple | | |
| 111 | +| **F12** | Aller là où il est déclaré | oui, dans le fichier | | |
| 112 | +| **Shift-F12** | Lister tous ses usages | oui, dans le fichier : sa déclaration et chaque appel | | |
| 113 | +| **Ctrl-T** | Trouver un symbole par son nom dans tout le projet | oui : les fonctions, unions et noms de module de premier niveau de chaque fichier `.golo` sous la racine du projet, ouvert ou non | | |
| 114 | + | |
| 115 | +Et, sans touche : *Symbol in file…* liste les fonctions et unions de premier niveau du fichier, les variantes de chaque union imbriquées sous elle ; *Problems…* liste tous les diagnostics ; *Find implementations…* répond par la déclaration de la fonction, là où va **F12** — Golo n'a pas d'interfaces, une fonction est sa propre implémentation ; *Go to type definition* répond « rien trouvé ». | |
| 116 | + | |
| 117 | +La seule qui ne répond rien est la frontière du serveur, pas celle de l'éditeur : `golo lsp` annonce `completion`, `hover`, `definition`, `documentSymbol`, `references`, `implementation` et `workspaceSymbol`, et pas `typeDefinition`. Jusqu'à GoloScript v0.2.0 il n'annonçait que les quatre premiers et ce tableau le disait ; un test de ce dépôt a échoué le jour où le serveur s'est mis à répondre aux trois autres, et c'est ainsi que le tableau a été revu. L'entrée de menu reste parce que la griser selon ce qu'un serveur a dit au démarrage donnerait au menu une forme différente selon la machine, et le même genre de test échoue encore le jour où un futur golo répond aussi à la définition de type. | |
| 118 | + | |
| 119 | +## Les marques d'erreur | |
| 120 | + | |
| 121 | +Les problèmes que le serveur trouve arrivent sans qu'on les demande, à l'ouverture et à chaque modification. La première erreur du fichier que vous éditez apparaît à droite de la barre d'état, précédée de `⚠` ; chaque ligne à problème reçoit un `×` dans la gouttière ; et **Code ▸ Problems…** les liste toutes. | |
| 122 | + | |
| 123 | +`golo lsp` en signale trois sortes : | |
| 124 | + | |
| 125 | +- **Les erreurs de syntaxe** du lexer et du parseur — une accolade manquante, un token que le parseur n'accepte pas. Les messages du parseur portent une ligne et pas de colonne, la marque tombe donc sur la ligne entière ; un message sans ligne du tout tombe sur la ligne 1. | |
| 126 | +- **Une confusion `:`/`.`** — `obj.method()` là où Golo veut `obj: method()`. | |
| 127 | +- **Un commentaire à la C** — `//` ou `/* */`, que Golo n'a pas. Les commentaires Golo sont `#` et `----`. | |
| 128 | + | |
| 129 | +Un script qui parse puis échoue à l'exécution ne reçoit aucune marque : le serveur parse, il n'exécute jamais rien. | |
| 130 | + | |
| 131 | +[Interroger le code](ask-about-code.md) parcourt le menu Code. | |
| 132 | + | |
| 133 | +## Voir aussi | |
| 134 | + | |
| 135 | +- Pourquoi le serveur est optionnel, et pourquoi l'interpréteur est le serveur : [Coloration et complétion](../explanation/colouring-and-completion.md) | |
| 136 | +- Installer l'interpréteur : [Installer GoloScript](install-goloscript.md) | |
| 137 | +- Toutes les touches : [référence du clavier](../reference/keyboard.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,137 @@ | |||
| 1 | +# Activer la complétion Golo | ||
| 2 | + | ||
| 3 | +Ce guide montre comment obtenir la complétion, les survols, l'aller-à-la-définition et les marques d'erreur. Il suppose que Turbo Golo est déjà installé. | ||
| 4 | + | ||
| 5 | +La complétion vient de **`golo lsp`** — l'interpréteur GoloScript lui-même, démarré en mode serveur de langage. Il n'y a pas de serveur séparé à installer : une machine qui peut exécuter un script Golo peut en compléter un. Turbo Golo n'embarque pas l'interpréteur pour autant : l'édition et la coloration marchent sans lui, et seules la complétion et les marques d'erreur manquent. | ||
| 6 | + | ||
| 7 | +## 1. Installer golo | ||
| 8 | + | ||
| 9 | +Soit un binaire depuis la [page des releases](https://codeberg.org/TypeUnsafe/golo-script/releases) : | ||
| 10 | + | ||
| 11 | +```bash | ||
| 12 | +chmod +x golo-<version>-<plateforme> | ||
| 13 | +sudo mv golo-<version>-<plateforme> /usr/local/bin/golo | ||
| 14 | +golo --version | ||
| 15 | +``` | ||
| 16 | + | ||
| 17 | +soit une construction depuis les sources, qui donne aussi les deux compilateurs : | ||
| 18 | + | ||
| 19 | +```bash | ||
| 20 | +git clone https://codeberg.org/TypeUnsafe/golo-script.git && cd golo-script | ||
| 21 | +./install.sh | ||
| 22 | +``` | ||
| 23 | + | ||
| 24 | +L'installeur de Turbo Golo fera la seconde pour vous : `scripts/install.sh --with-server`. [Installer GoloScript](install-goloscript.md) donne le détail. | ||
| 25 | + | ||
| 26 | +## 2. S'assurer que Turbo Golo le trouve | ||
| 27 | + | ||
| 28 | +Turbo Golo regarde d'abord sur le `PATH`, puis dans `/usr/local/bin`, où écrit l'installeur de GoloScript. Vérifiez : | ||
| 29 | + | ||
| 30 | +```bash | ||
| 31 | +golo --version | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +``` | ||
| 35 | +v0.1.1 | dev.20260802.🤓 | ||
| 36 | +``` | ||
| 37 | + | ||
| 38 | +Si cela dit « command not found » mais que Turbo Golo le trouve quand même, c'est attendu et sans conséquence : l'éditeur a cherché dans `/usr/local/bin` lui-même. | ||
| 39 | + | ||
| 40 | +## 3. Ouvrir un script | ||
| 41 | + | ||
| 42 | +```bash | ||
| 43 | +cd /chemin/vers/vos/scripts | ||
| 44 | +turbo-golo main.golo | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +Golo n'a pas de manifeste de projet, il n'y a donc rien à chercher : `golo lsp` est démarré dans le répertoire du fichier que vous avez ouvert, et il répond à propos de ce fichier. L'endroit d'où vous lancez l'éditeur ne change rien à la complétion — il décide où s'exécutent les commandes du menu Golo, ce qui est une autre affaire. | ||
| 48 | + | ||
| 49 | +## 4. Demander une complétion | ||
| 50 | + | ||
| 51 | +Tapez les premières lettres d'un nom et appuyez sur **Ctrl-Espace** : | ||
| 52 | + | ||
| 53 | +```golo | ||
| 54 | +prin | ||
| 55 | +``` | ||
| 56 | + | ||
| 57 | +Une liste se déroule sous le curseur — `print`, `println`, avec leurs signatures. Continuez à taper pour la réduire, **↑ ↓** pour la parcourir, **Entrée** ou **Tab** pour accepter, **Échap** pour l'écarter. | ||
| 58 | + | ||
| 59 | +## Ce que contient la liste | ||
| 60 | + | ||
| 61 | +`golo lsp` propose quatre sortes de choses : | ||
| 62 | + | ||
| 63 | +| Proposé | Exemple | | ||
| 64 | +| --- | --- | | ||
| 65 | +| Les mots-clés | `function`, `foreach`, `augment` | | ||
| 66 | +| Les builtins de l'interpréteur, avec leurs signatures et leur documentation | `println`, `readFile`, `httpGet`, `DynamicObject` | | ||
| 67 | +| Les fonctions et unions déclarées **au premier niveau** du fichier | votre propre `function helper = …` | | ||
| 68 | +| Les symboles apportés par `import` depuis les modules embarqués dans le binaire | `Some`, `None`, `isSome`, `either` après `import gololang.Errors` | | ||
| 69 | + | ||
| 70 | +Deux choses n'y sont délibérément **pas**, et toutes deux ressemblent à un serveur cassé quand on ne le sait pas : | ||
| 71 | + | ||
| 72 | +- **Une fonction déclarée dans une autre fonction.** Seules les déclarations de premier niveau sont collectées. Sortez-la, ou acceptez qu'elle ne soit pas proposée. | ||
| 73 | +- **Tout ce qui vient d'un fichier `.golo` à vous.** `import` résout les modules intégrés à l'interpréteur — `gololang.Errors`, `gololang.Types`, `gololang.Ui`, `gololang.Testing`, … — et rien sur le disque. Une fonction dans `lib/util.golo` n'est pas vue depuis `main.golo`. | ||
| 74 | + | ||
| 75 | +## Vérifier ce que fait le serveur | ||
| 76 | + | ||
| 77 | +L'extrémité droite de la barre d'état montre l'état du serveur de langage : `LSP: starting…`, `LSP: ready`, ou la raison pour laquelle il n'y en a pas : | ||
| 78 | + | ||
| 79 | +``` | ||
| 80 | +LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases | ||
| 81 | +``` | ||
| 82 | + | ||
| 83 | +`Run ▸ Language server status` montre la même chose dans une boîte, avec le chemin où le binaire a été trouvé et le répertoire où il a été démarré. | ||
| 84 | + | ||
| 85 | +## Variantes | ||
| 86 | + | ||
| 87 | +**Vous ne voulez aucun serveur de langage :** | ||
| 88 | + | ||
| 89 | +```bash | ||
| 90 | +turbo-golo -no-lsp main.golo | ||
| 91 | +``` | ||
| 92 | + | ||
| 93 | +**La complétion est morte dans une fenêtre née sans nom.** Une fenêtre « Untitled » n'a aucun fichier à annoncer à `golo lsp` tant qu'elle n'est pas enregistrée — appuyez sur **F2** et donnez-lui un nom en `.golo`. Dès cette sauvegarde, la complétion, le survol et les marques d'erreur fonctionnent dans cette fenêtre ; inutile de quitter et relancer l'éditeur. | ||
| 94 | + | ||
| 95 | +**La liste est vide.** `golo lsp` répond pour n'importe quel fichier, y compris un qui ne parse pas — il liste les mots-clés et les builtins quoi qu'il arrive — une liste vide signifie donc presque toujours que le serveur ne tourne pas. Lisez la barre d'état. | ||
| 96 | + | ||
| 97 | +**Ctrl-Espace ne fait rien.** tmux, screen et les terminaux des IDE s'approprient fréquemment `Ctrl-Espace` avant que l'éditeur le voie. Utilisez `Run ▸ Completion` à la place. | ||
| 98 | + | ||
| 99 | +**Une requête prend trop longtemps.** Chaque requête abandonne après quelques secondes, un serveur bloqué ralentit donc l'éditeur mais ne le fige jamais. La barre d'état signale l'échec. | ||
| 100 | + | ||
| 101 | +**Vous avez installé golo à un endroit inhabituel.** L'éditeur cherche sur le `PATH` et dans `/usr/local/bin`, et nulle part ailleurs — aucune variable d'environnement ne nomme un autre répertoire. Mettez le répertoire sur le `PATH`, ou un lien symbolique dans `/usr/local/bin`. | ||
| 102 | + | ||
| 103 | +## Ce que le serveur donne d'autre | ||
| 104 | + | ||
| 105 | +La complétion est la chose la plus bruyante qu'il fait et la moindre de ce qu'il sait. La même connexion répond à quatre autres questions, toutes dans le menu **Code** — trois à propos du symbole sous le curseur, aucune sélection nécessaire, et une à propos d'un nom que vous tapez. | ||
| 106 | + | ||
| 107 | +| Touche | Ce qu'elle fait | Avec `golo lsp` | | ||
| 108 | +| --- | --- | --- | | ||
| 109 | +| **Ctrl-Espace** | La liste de complétion | oui | | ||
| 110 | +| **F1** | Décrire le symbole sous le curseur | oui — pour une fonction que vous avez déclarée, les commentaires `#` écrits juste au-dessus ; pour un builtin, sa signature et un exemple | | ||
| 111 | +| **F12** | Aller là où il est déclaré | oui, dans le fichier | | ||
| 112 | +| **Shift-F12** | Lister tous ses usages | oui, dans le fichier : sa déclaration et chaque appel | | ||
| 113 | +| **Ctrl-T** | Trouver un symbole par son nom dans tout le projet | oui : les fonctions, unions et noms de module de premier niveau de chaque fichier `.golo` sous la racine du projet, ouvert ou non | | ||
| 114 | + | ||
| 115 | +Et, sans touche : *Symbol in file…* liste les fonctions et unions de premier niveau du fichier, les variantes de chaque union imbriquées sous elle ; *Problems…* liste tous les diagnostics ; *Find implementations…* répond par la déclaration de la fonction, là où va **F12** — Golo n'a pas d'interfaces, une fonction est sa propre implémentation ; *Go to type definition* répond « rien trouvé ». | ||
| 116 | + | ||
| 117 | +La seule qui ne répond rien est la frontière du serveur, pas celle de l'éditeur : `golo lsp` annonce `completion`, `hover`, `definition`, `documentSymbol`, `references`, `implementation` et `workspaceSymbol`, et pas `typeDefinition`. Jusqu'à GoloScript v0.2.0 il n'annonçait que les quatre premiers et ce tableau le disait ; un test de ce dépôt a échoué le jour où le serveur s'est mis à répondre aux trois autres, et c'est ainsi que le tableau a été revu. L'entrée de menu reste parce que la griser selon ce qu'un serveur a dit au démarrage donnerait au menu une forme différente selon la machine, et le même genre de test échoue encore le jour où un futur golo répond aussi à la définition de type. | ||
| 118 | + | ||
| 119 | +## Les marques d'erreur | ||
| 120 | + | ||
| 121 | +Les problèmes que le serveur trouve arrivent sans qu'on les demande, à l'ouverture et à chaque modification. La première erreur du fichier que vous éditez apparaît à droite de la barre d'état, précédée de `⚠` ; chaque ligne à problème reçoit un `×` dans la gouttière ; et **Code ▸ Problems…** les liste toutes. | ||
| 122 | + | ||
| 123 | +`golo lsp` en signale trois sortes : | ||
| 124 | + | ||
| 125 | +- **Les erreurs de syntaxe** du lexer et du parseur — une accolade manquante, un token que le parseur n'accepte pas. Les messages du parseur portent une ligne et pas de colonne, la marque tombe donc sur la ligne entière ; un message sans ligne du tout tombe sur la ligne 1. | ||
| 126 | +- **Une confusion `:`/`.`** — `obj.method()` là où Golo veut `obj: method()`. | ||
| 127 | +- **Un commentaire à la C** — `//` ou `/* */`, que Golo n'a pas. Les commentaires Golo sont `#` et `----`. | ||
| 128 | + | ||
| 129 | +Un script qui parse puis échoue à l'exécution ne reçoit aucune marque : le serveur parse, il n'exécute jamais rien. | ||
| 130 | + | ||
| 131 | +[Interroger le code](ask-about-code.md) parcourt le menu Code. | ||
| 132 | + | ||
| 133 | +## Voir aussi | ||
| 134 | + | ||
| 135 | +- Pourquoi le serveur est optionnel, et pourquoi l'interpréteur est le serveur : [Coloration et complétion](../explanation/colouring-and-completion.md) | ||
| 136 | +- Installer l'interpréteur : [Installer GoloScript](install-goloscript.md) | ||
| 137 | +- Toutes les touches : [référence du clavier](../reference/keyboard.md) | ||
added
docs/fr/how-to/install-goloscript.md +100 -0 | new file mode 100644 | ||
| @@ -0,0 +1,100 @@ | ||
| 1 | +# Installer GoloScript | |
| 2 | + | |
| 3 | +Ce guide montre comment mettre `golo` — et, si vous les voulez, `gogolo` et `wagolo` — sur une machine, et comment vérifier que Turbo Golo le trouve. Il suppose que vous avez déjà Turbo Golo, ou êtes sur le point de l'avoir — voir [installer l'éditeur](install.md) pour cela. | |
| 4 | + | |
| 5 | +**L'éditeur marche sans rien de tout cela.** L'édition, la coloration, les thèmes, les snippets et les fenêtres de terminal tournent tous sans aucun interpréteur. Ce qui en a besoin : la complétion, les marques d'erreur dans la gouttière, et chaque commande du menu Golo. | |
| 6 | + | |
| 7 | +## Quel binaire il vous faut | |
| 8 | + | |
| 9 | +GoloScript en livre trois, sous une seule version : | |
| 10 | + | |
| 11 | +| Binaire | À quoi il sert | Il lui faut aussi | | |
| 12 | +| --- | --- | --- | | |
| 13 | +| `golo` | L'interpréteur : exécute les scripts, et est aussi le REPL, le débogueur, le lanceur de tests et **le serveur de langage** | rien | | |
| 14 | +| `gogolo` | Compile un script en exécutable natif, via Go | l'outillage Go | | |
| 15 | +| `wagolo` | Compile un script en WebAssembly, via Go et TinyGo | TinyGo, et `wasm-tools` pour la cible `wasip2` | | |
| 16 | + | |
| 17 | +L'éditeur a besoin de `golo` et de rien d'autre. `gogolo` et `wagolo` sont ce que lancent les entrées **Build native** et **Build wasm** du menu Golo ; sans eux ces deux entrées répondent `command not found` et le reste du menu n'est pas affecté. | |
| 18 | + | |
| 19 | +## Installer une release | |
| 20 | + | |
| 21 | +Des binaires précompilés pour macOS (Intel et Apple Silicon), Linux (amd64, arm64, 386) et Windows (amd64, arm64, 386) sont sur la [page des releases](https://codeberg.org/TypeUnsafe/golo-script/releases). Chacun est un fichier : | |
| 22 | + | |
| 23 | +```bash | |
| 24 | +chmod +x golo-<version>-<plateforme> | |
| 25 | +sudo mv golo-<version>-<plateforme> /usr/local/bin/golo | |
| 26 | +golo --version | |
| 27 | +``` | |
| 28 | + | |
| 29 | +Sur macOS un téléchargement non signé est mis en quarantaine jusqu'à ce que vous en décidiez autrement, dans **Réglages Système ▸ Confidentialité et sécurité** ou avec `xattr -d com.apple.quarantine golo-<version>-<plateforme>`. | |
| 30 | + | |
| 31 | +Répétez pour `gogolo` et `wagolo` si vous voulez les compilateurs. | |
| 32 | + | |
| 33 | +## Ou construire depuis les sources | |
| 34 | + | |
| 35 | +Cela donne les trois d'un coup, dans `/usr/local/bin`. Il faut Go ; `wagolo` se construit sans TinyGo mais ne peut rien compiler tant que TinyGo n'est pas installé. | |
| 36 | + | |
| 37 | +```bash | |
| 38 | +git clone https://codeberg.org/TypeUnsafe/golo-script.git && cd golo-script | |
| 39 | +./install.sh | |
| 40 | +``` | |
| 41 | + | |
| 42 | +L'installeur de Turbo Golo fera exactement cela si on le lui demande : | |
| 43 | + | |
| 44 | +```bash | |
| 45 | +scripts/install.sh --with-server | |
| 46 | +``` | |
| 47 | + | |
| 48 | +Il clone GoloScript dans un répertoire temporaire et lance son `install.sh`, qui demande `sudo` quand il copie dans `/usr/local/bin`. | |
| 49 | + | |
| 50 | +## Vérifier | |
| 51 | + | |
| 52 | +```bash | |
| 53 | +golo --version | |
| 54 | +``` | |
| 55 | + | |
| 56 | +``` | |
| 57 | +v0.1.1 | dev.20260802.🤓 | |
| 58 | +``` | |
| 59 | + | |
| 60 | +Et le serveur de langage en particulier — c'est l'interpréteur, c'est donc une sous-commande plutôt qu'un second binaire : | |
| 61 | + | |
| 62 | +```bash | |
| 63 | +golo lsp </dev/null | |
| 64 | +``` | |
| 65 | + | |
| 66 | +Il ne lit rien, voit la fin de son entrée, et se termine proprement. Un `golo` qui imprime son usage ou démarre un REPL ici est un autre programme du même nom. | |
| 67 | + | |
| 68 | +## Vérifier que l'éditeur le trouve | |
| 69 | + | |
| 70 | +Ouvrez n'importe quel fichier `.golo` et lisez l'extrémité droite de la barre d'état : | |
| 71 | + | |
| 72 | +``` | |
| 73 | + F1 Describe F2 Save F3 Open F6 Window F10 Menu 1:1 LSP: ready | |
| 74 | +``` | |
| 75 | + | |
| 76 | +`LSP: ready` signifie que le serveur a démarré. `LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases` signifie qu'il n'a pas été trouvé, et le message dit où l'obtenir. | |
| 77 | + | |
| 78 | +**Turbo Golo regarde à deux endroits, dans l'ordre** : votre `PATH`, puis `/usr/local/bin`. Le second est là où écrit l'installeur de GoloScript, et il est fouillé même quand il n'est pas sur le `PATH` — un shell lancé par un lanceur de bureau, par exemple — pour que le cas qui ressemble sinon à un serveur cassé fonctionne. | |
| 79 | + | |
| 80 | +## Variantes | |
| 81 | + | |
| 82 | +- **Vous installez les binaires ailleurs.** Mettez ce répertoire sur le `PATH` ; l'éditeur n'a aucune variable d'environnement nommant un autre endroit où regarder. Un lien symbolique dans `/usr/local/bin` marche aussi. | |
| 83 | +- **Vous avez déjà `golo` mais pas de complétion.** Lancez `golo lsp </dev/null` et assurez-vous qu'il se termine sans bruit. Puis lisez la barre d'état — `Run ▸ Language server status` montre le chemin que l'éditeur a trouvé et si le serveur a répondu à sa poignée de main. | |
| 84 | +- **Vous voulez mettre à jour.** Remplacez le binaire — un téléchargement de release ou `./install.sh` à nouveau après un `git pull`. Redémarrez l'éditeur : il démarre un serveur par session et ne remarque pas un nouveau binaire avant. | |
| 85 | +- **Vous installez pour la CI, ou dans une image.** GoloScript publie une image basée sur `scratch` qui ne contient que l'interpréteur : `docker run --rm -v "$PWD:/app" -w /app k33g/gololang:<tag> /golo ./main.golo`. L'éditeur ne peut pas utiliser un serveur dans un conteneur, c'est donc pour exécuter des scripts, pas pour la complétion. | |
| 86 | +- **Vous voulez être sûr que l'éditeur ne le trouve pas simplement sur le `PATH`.** Lancez-le avec un environnement réduit — `env PATH=/usr/bin:/bin turbo-golo main.golo` — et la barre d'état doit toujours dire `LSP: ready`, depuis `/usr/local/bin`. | |
| 87 | + | |
| 88 | +## À quoi sert chaque binaire, vu de l'éditeur | |
| 89 | + | |
| 90 | +| Binaire | Ce que l'éditeur en fait | | |
| 91 | +| --- | --- | | |
| 92 | +| `golo` | `golo lsp` — la complétion, le survol, les définitions, les symboles du fichier et les marques d'erreur ; et les entrées **Run**, **Test**, **Test one**, **Debug**, **REPL** et **New script** du menu Golo | | |
| 93 | +| `gogolo` | L'entrée **Build native** | | |
| 94 | +| `wagolo` | L'entrée **Build wasm** | | |
| 95 | + | |
| 96 | +## Voir aussi | |
| 97 | + | |
| 98 | +- [Activer la complétion](enable-completion.md) — que faire quand le serveur est installé et ne dit toujours rien | |
| 99 | +- [Lancer des commandes Golo depuis l'éditeur](run-golo-commands.md) — le menu Golo | |
| 100 | +- [Coloration et complétion](../explanation/colouring-and-completion.md) — pourquoi l'interpréteur est le serveur | |
| new file mode 100644 | |||
| @@ -0,0 +1,100 @@ | |||
| 1 | +# Installer GoloScript | ||
| 2 | + | ||
| 3 | +Ce guide montre comment mettre `golo` — et, si vous les voulez, `gogolo` et `wagolo` — sur une machine, et comment vérifier que Turbo Golo le trouve. Il suppose que vous avez déjà Turbo Golo, ou êtes sur le point de l'avoir — voir [installer l'éditeur](install.md) pour cela. | ||
| 4 | + | ||
| 5 | +**L'éditeur marche sans rien de tout cela.** L'édition, la coloration, les thèmes, les snippets et les fenêtres de terminal tournent tous sans aucun interpréteur. Ce qui en a besoin : la complétion, les marques d'erreur dans la gouttière, et chaque commande du menu Golo. | ||
| 6 | + | ||
| 7 | +## Quel binaire il vous faut | ||
| 8 | + | ||
| 9 | +GoloScript en livre trois, sous une seule version : | ||
| 10 | + | ||
| 11 | +| Binaire | À quoi il sert | Il lui faut aussi | | ||
| 12 | +| --- | --- | --- | | ||
| 13 | +| `golo` | L'interpréteur : exécute les scripts, et est aussi le REPL, le débogueur, le lanceur de tests et **le serveur de langage** | rien | | ||
| 14 | +| `gogolo` | Compile un script en exécutable natif, via Go | l'outillage Go | | ||
| 15 | +| `wagolo` | Compile un script en WebAssembly, via Go et TinyGo | TinyGo, et `wasm-tools` pour la cible `wasip2` | | ||
| 16 | + | ||
| 17 | +L'éditeur a besoin de `golo` et de rien d'autre. `gogolo` et `wagolo` sont ce que lancent les entrées **Build native** et **Build wasm** du menu Golo ; sans eux ces deux entrées répondent `command not found` et le reste du menu n'est pas affecté. | ||
| 18 | + | ||
| 19 | +## Installer une release | ||
| 20 | + | ||
| 21 | +Des binaires précompilés pour macOS (Intel et Apple Silicon), Linux (amd64, arm64, 386) et Windows (amd64, arm64, 386) sont sur la [page des releases](https://codeberg.org/TypeUnsafe/golo-script/releases). Chacun est un fichier : | ||
| 22 | + | ||
| 23 | +```bash | ||
| 24 | +chmod +x golo-<version>-<plateforme> | ||
| 25 | +sudo mv golo-<version>-<plateforme> /usr/local/bin/golo | ||
| 26 | +golo --version | ||
| 27 | +``` | ||
| 28 | + | ||
| 29 | +Sur macOS un téléchargement non signé est mis en quarantaine jusqu'à ce que vous en décidiez autrement, dans **Réglages Système ▸ Confidentialité et sécurité** ou avec `xattr -d com.apple.quarantine golo-<version>-<plateforme>`. | ||
| 30 | + | ||
| 31 | +Répétez pour `gogolo` et `wagolo` si vous voulez les compilateurs. | ||
| 32 | + | ||
| 33 | +## Ou construire depuis les sources | ||
| 34 | + | ||
| 35 | +Cela donne les trois d'un coup, dans `/usr/local/bin`. Il faut Go ; `wagolo` se construit sans TinyGo mais ne peut rien compiler tant que TinyGo n'est pas installé. | ||
| 36 | + | ||
| 37 | +```bash | ||
| 38 | +git clone https://codeberg.org/TypeUnsafe/golo-script.git && cd golo-script | ||
| 39 | +./install.sh | ||
| 40 | +``` | ||
| 41 | + | ||
| 42 | +L'installeur de Turbo Golo fera exactement cela si on le lui demande : | ||
| 43 | + | ||
| 44 | +```bash | ||
| 45 | +scripts/install.sh --with-server | ||
| 46 | +``` | ||
| 47 | + | ||
| 48 | +Il clone GoloScript dans un répertoire temporaire et lance son `install.sh`, qui demande `sudo` quand il copie dans `/usr/local/bin`. | ||
| 49 | + | ||
| 50 | +## Vérifier | ||
| 51 | + | ||
| 52 | +```bash | ||
| 53 | +golo --version | ||
| 54 | +``` | ||
| 55 | + | ||
| 56 | +``` | ||
| 57 | +v0.1.1 | dev.20260802.🤓 | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +Et le serveur de langage en particulier — c'est l'interpréteur, c'est donc une sous-commande plutôt qu'un second binaire : | ||
| 61 | + | ||
| 62 | +```bash | ||
| 63 | +golo lsp </dev/null | ||
| 64 | +``` | ||
| 65 | + | ||
| 66 | +Il ne lit rien, voit la fin de son entrée, et se termine proprement. Un `golo` qui imprime son usage ou démarre un REPL ici est un autre programme du même nom. | ||
| 67 | + | ||
| 68 | +## Vérifier que l'éditeur le trouve | ||
| 69 | + | ||
| 70 | +Ouvrez n'importe quel fichier `.golo` et lisez l'extrémité droite de la barre d'état : | ||
| 71 | + | ||
| 72 | +``` | ||
| 73 | + F1 Describe F2 Save F3 Open F6 Window F10 Menu 1:1 LSP: ready | ||
| 74 | +``` | ||
| 75 | + | ||
| 76 | +`LSP: ready` signifie que le serveur a démarré. `LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases` signifie qu'il n'a pas été trouvé, et le message dit où l'obtenir. | ||
| 77 | + | ||
| 78 | +**Turbo Golo regarde à deux endroits, dans l'ordre** : votre `PATH`, puis `/usr/local/bin`. Le second est là où écrit l'installeur de GoloScript, et il est fouillé même quand il n'est pas sur le `PATH` — un shell lancé par un lanceur de bureau, par exemple — pour que le cas qui ressemble sinon à un serveur cassé fonctionne. | ||
| 79 | + | ||
| 80 | +## Variantes | ||
| 81 | + | ||
| 82 | +- **Vous installez les binaires ailleurs.** Mettez ce répertoire sur le `PATH` ; l'éditeur n'a aucune variable d'environnement nommant un autre endroit où regarder. Un lien symbolique dans `/usr/local/bin` marche aussi. | ||
| 83 | +- **Vous avez déjà `golo` mais pas de complétion.** Lancez `golo lsp </dev/null` et assurez-vous qu'il se termine sans bruit. Puis lisez la barre d'état — `Run ▸ Language server status` montre le chemin que l'éditeur a trouvé et si le serveur a répondu à sa poignée de main. | ||
| 84 | +- **Vous voulez mettre à jour.** Remplacez le binaire — un téléchargement de release ou `./install.sh` à nouveau après un `git pull`. Redémarrez l'éditeur : il démarre un serveur par session et ne remarque pas un nouveau binaire avant. | ||
| 85 | +- **Vous installez pour la CI, ou dans une image.** GoloScript publie une image basée sur `scratch` qui ne contient que l'interpréteur : `docker run --rm -v "$PWD:/app" -w /app k33g/gololang:<tag> /golo ./main.golo`. L'éditeur ne peut pas utiliser un serveur dans un conteneur, c'est donc pour exécuter des scripts, pas pour la complétion. | ||
| 86 | +- **Vous voulez être sûr que l'éditeur ne le trouve pas simplement sur le `PATH`.** Lancez-le avec un environnement réduit — `env PATH=/usr/bin:/bin turbo-golo main.golo` — et la barre d'état doit toujours dire `LSP: ready`, depuis `/usr/local/bin`. | ||
| 87 | + | ||
| 88 | +## À quoi sert chaque binaire, vu de l'éditeur | ||
| 89 | + | ||
| 90 | +| Binaire | Ce que l'éditeur en fait | | ||
| 91 | +| --- | --- | | ||
| 92 | +| `golo` | `golo lsp` — la complétion, le survol, les définitions, les symboles du fichier et les marques d'erreur ; et les entrées **Run**, **Test**, **Test one**, **Debug**, **REPL** et **New script** du menu Golo | | ||
| 93 | +| `gogolo` | L'entrée **Build native** | | ||
| 94 | +| `wagolo` | L'entrée **Build wasm** | | ||
| 95 | + | ||
| 96 | +## Voir aussi | ||
| 97 | + | ||
| 98 | +- [Activer la complétion](enable-completion.md) — que faire quand le serveur est installé et ne dit toujours rien | ||
| 99 | +- [Lancer des commandes Golo depuis l'éditeur](run-golo-commands.md) — le menu Golo | ||
| 100 | +- [Coloration et complétion](../explanation/colouring-and-completion.md) — pourquoi l'interpréteur est le serveur | ||
added
docs/fr/how-to/install.md +96 -0 | new file mode 100644 | ||
| @@ -0,0 +1,96 @@ | ||
| 1 | +# Installer et compiler Turbo Golo | |
| 2 | + | |
| 3 | +Ce guide montre comment obtenir un binaire `turbo-golo` fonctionnel. Il suppose que vous avez Go 1.26 ou plus récent et que vous savez utiliser un terminal. | |
| 4 | + | |
| 5 | +## La voie rapide, depuis un clone | |
| 6 | + | |
| 7 | +```bash | |
| 8 | +git clone ssh://git@rickub.com/turbo-editors/turbo-golo.git | |
| 9 | +cd turbo-golo | |
| 10 | +make install | |
| 11 | +``` | |
| 12 | + | |
| 13 | +Cela compile l'éditeur, le place là où votre shell cherche ses commandes, et vous dit ce qu'il a trouvé : la version de Go, où le binaire a été posé, si ce répertoire est dans votre `PATH`, et si `golo` — l'interpréteur GoloScript, qui est aussi le serveur de langage — est installé. La compilation passe d'abord par un fichier temporaire : un échec ne remplace jamais une installation qui marchait. | |
| 14 | + | |
| 15 | +Ensuite, depuis n'importe quel dossier contenant un script Golo : | |
| 16 | + | |
| 17 | +```bash | |
| 18 | +turbo-golo main.golo | |
| 19 | +``` | |
| 20 | + | |
| 21 | +### Options | |
| 22 | + | |
| 23 | +```bash | |
| 24 | +scripts/install.sh --prefix ~/bin # installer ailleurs | |
| 25 | +scripts/install.sh --with-server # construire et installer GoloScript aussi | |
| 26 | +scripts/install.sh --uninstall # le retirer (make uninstall) | |
| 27 | +scripts/install.sh --help | |
| 28 | +``` | |
| 29 | + | |
| 30 | +Sans `--prefix`, l'éditeur va là où `go install` le mettrait : `$GOBIN`, ou `$GOPATH/bin` si `GOBIN` n'est pas défini — le plus souvent `~/go/bin`. | |
| 31 | + | |
| 32 | +`--with-server` clone `https://codeberg.org/TypeUnsafe/golo-script` et lance son propre installeur, qui dépose `golo`, `gogolo` et `wagolo` dans `/usr/local/bin`. Il a besoin de `git` et de Go. Si vous préférez un binaire précompilé, voir [Installer GoloScript](install-goloscript.md). | |
| 33 | + | |
| 34 | +## Seulement compiler, sans installer | |
| 35 | + | |
| 36 | +```bash | |
| 37 | +make build | |
| 38 | +./bin/turbo-golo main.golo | |
| 39 | +``` | |
| 40 | + | |
| 41 | +`make build` vérifie aussitôt que le binaire produit annonce la version attendue ; c'est pour cela qu'il appelle `scripts/check-version.sh` derrière `go build`. | |
| 42 | + | |
| 43 | +## Depuis le proxy de modules, sans clone | |
| 44 | + | |
| 45 | +```bash | |
| 46 | +go install rickub.com/turbo-editors/turbo-golo@latest | |
| 47 | +``` | |
| 48 | + | |
| 49 | +Si la commande est ensuite « introuvable », c'est que le répertoire d'installation n'est pas dans votre `PATH` : | |
| 50 | + | |
| 51 | +```bash | |
| 52 | +export PATH="$PATH:$(go env GOPATH)/bin" | |
| 53 | +``` | |
| 54 | + | |
| 55 | +## Vérifier que ça marche | |
| 56 | + | |
| 57 | +```bash | |
| 58 | +turbo-golo -version | |
| 59 | +turbo-golo -list-themes | |
| 60 | +``` | |
| 61 | + | |
| 62 | +La première nomme le commit dont le binaire a été construit, ce qu'il faut citer dans un rapport de bug ; [le numéro de version](../reference/versioning.md) explique ce que signifie chaque forme. La seconde affiche les thèmes compilés dans le binaire et vous indique où placer les vôtres. | |
| 63 | + | |
| 64 | +## Variantes | |
| 65 | + | |
| 66 | +- **Vous voulez juste l'essayer une fois** : `go run rickub.com/turbo-editors/turbo-golo@latest main.golo` | |
| 67 | +- **Vous voulez le binaire à un endroit précis** : `go build -o /usr/local/bin/turbo-golo .` | |
| 68 | +- **Votre terminal ne gère pas les couleurs 24 bits** : utilisez `turbo-golo -theme turbo-classic`, construit uniquement sur les seize couleurs ANSI. `turbo-dark` et `borland-light` utilisent des couleurs 24 bits. | |
| 69 | + | |
| 70 | +## Quand quelque chose ne va pas | |
| 71 | + | |
| 72 | +**`the installed binary does not run`.** L'installeur affiche juste au-dessus ce que le système a répondu — lisez-le d'abord, c'est lui qui nomme le vrai problème. | |
| 73 | + | |
| 74 | +L'installeur **remplace** le binaire au lieu d'écrire par-dessus celui qui est là : une réinstallation donne donc au fichier une identité neuve. Cela compte sous macOS, qui met en cache la signature de code d'un binaire **par inode** : écrire de nouveaux octets dans l'ancien inode laisse une signature en cache qui décrit autre chose, et le noyau refuse alors d'exécuter un binaire qui s'est pourtant compilé et installé sans erreur. Si une ancienne copie a été installée par un outil qui employait `cp`, la supprimer d'abord efface cet état : | |
| 75 | + | |
| 76 | +```bash | |
| 77 | +scripts/install.sh --uninstall | |
| 78 | +scripts/install.sh | |
| 79 | +``` | |
| 80 | + | |
| 81 | +**`Go x.y or later is needed`.** La version vient de `go.mod` : elle ne peut donc pas diverger de ce dont le code a réellement besoin. Mettez Go à jour, ou compilez depuis une étiquette correspondant à la chaîne d'outils dont vous disposez. | |
| 82 | + | |
| 83 | +**`build failed; nothing was installed`.** Votre installation existante est intacte — la compilation passe d'abord par un fichier temporaire. La sortie du compilateur est affichée au-dessus du message. | |
| 84 | + | |
| 85 | +**La barre d'état dit `LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases`.** L'éditeur fonctionne, mais sans complétion : `golo` n'est ni dans le `PATH` ni dans `/usr/local/bin`. Voir [Installer GoloScript](install-goloscript.md). | |
| 86 | + | |
| 87 | +## Exigences côté terminal | |
| 88 | + | |
| 89 | +Turbo Golo a besoin d'un terminal qui rapporte sa taille et gère la souris — tous les terminaux courants le font. Il lit `TERM` via tcell ; si l'affichage est incorrect, vérifiez que `TERM` correspond bien à votre terminal (`xterm-256color` est une valeur sûre). | |
| 90 | + | |
| 91 | +## Voir aussi | |
| 92 | + | |
| 93 | +- Toutes les options : [référence de la ligne de commande](../reference/cli.md) | |
| 94 | +- Faire marcher la complétion : [Activer la complétion Golo](enable-completion.md) | |
| 95 | +- Une première session guidée : [Votre premier programme Golo dans Turbo Golo](../tutorials/getting-started.md) | |
| 96 | +- Des projets pour l'essayer : [les démos](../../../demos/) | |
| new file mode 100644 | |||
| @@ -0,0 +1,96 @@ | |||
| 1 | +# Installer et compiler Turbo Golo | ||
| 2 | + | ||
| 3 | +Ce guide montre comment obtenir un binaire `turbo-golo` fonctionnel. Il suppose que vous avez Go 1.26 ou plus récent et que vous savez utiliser un terminal. | ||
| 4 | + | ||
| 5 | +## La voie rapide, depuis un clone | ||
| 6 | + | ||
| 7 | +```bash | ||
| 8 | +git clone ssh://git@rickub.com/turbo-editors/turbo-golo.git | ||
| 9 | +cd turbo-golo | ||
| 10 | +make install | ||
| 11 | +``` | ||
| 12 | + | ||
| 13 | +Cela compile l'éditeur, le place là où votre shell cherche ses commandes, et vous dit ce qu'il a trouvé : la version de Go, où le binaire a été posé, si ce répertoire est dans votre `PATH`, et si `golo` — l'interpréteur GoloScript, qui est aussi le serveur de langage — est installé. La compilation passe d'abord par un fichier temporaire : un échec ne remplace jamais une installation qui marchait. | ||
| 14 | + | ||
| 15 | +Ensuite, depuis n'importe quel dossier contenant un script Golo : | ||
| 16 | + | ||
| 17 | +```bash | ||
| 18 | +turbo-golo main.golo | ||
| 19 | +``` | ||
| 20 | + | ||
| 21 | +### Options | ||
| 22 | + | ||
| 23 | +```bash | ||
| 24 | +scripts/install.sh --prefix ~/bin # installer ailleurs | ||
| 25 | +scripts/install.sh --with-server # construire et installer GoloScript aussi | ||
| 26 | +scripts/install.sh --uninstall # le retirer (make uninstall) | ||
| 27 | +scripts/install.sh --help | ||
| 28 | +``` | ||
| 29 | + | ||
| 30 | +Sans `--prefix`, l'éditeur va là où `go install` le mettrait : `$GOBIN`, ou `$GOPATH/bin` si `GOBIN` n'est pas défini — le plus souvent `~/go/bin`. | ||
| 31 | + | ||
| 32 | +`--with-server` clone `https://codeberg.org/TypeUnsafe/golo-script` et lance son propre installeur, qui dépose `golo`, `gogolo` et `wagolo` dans `/usr/local/bin`. Il a besoin de `git` et de Go. Si vous préférez un binaire précompilé, voir [Installer GoloScript](install-goloscript.md). | ||
| 33 | + | ||
| 34 | +## Seulement compiler, sans installer | ||
| 35 | + | ||
| 36 | +```bash | ||
| 37 | +make build | ||
| 38 | +./bin/turbo-golo main.golo | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +`make build` vérifie aussitôt que le binaire produit annonce la version attendue ; c'est pour cela qu'il appelle `scripts/check-version.sh` derrière `go build`. | ||
| 42 | + | ||
| 43 | +## Depuis le proxy de modules, sans clone | ||
| 44 | + | ||
| 45 | +```bash | ||
| 46 | +go install rickub.com/turbo-editors/turbo-golo@latest | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +Si la commande est ensuite « introuvable », c'est que le répertoire d'installation n'est pas dans votre `PATH` : | ||
| 50 | + | ||
| 51 | +```bash | ||
| 52 | +export PATH="$PATH:$(go env GOPATH)/bin" | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +## Vérifier que ça marche | ||
| 56 | + | ||
| 57 | +```bash | ||
| 58 | +turbo-golo -version | ||
| 59 | +turbo-golo -list-themes | ||
| 60 | +``` | ||
| 61 | + | ||
| 62 | +La première nomme le commit dont le binaire a été construit, ce qu'il faut citer dans un rapport de bug ; [le numéro de version](../reference/versioning.md) explique ce que signifie chaque forme. La seconde affiche les thèmes compilés dans le binaire et vous indique où placer les vôtres. | ||
| 63 | + | ||
| 64 | +## Variantes | ||
| 65 | + | ||
| 66 | +- **Vous voulez juste l'essayer une fois** : `go run rickub.com/turbo-editors/turbo-golo@latest main.golo` | ||
| 67 | +- **Vous voulez le binaire à un endroit précis** : `go build -o /usr/local/bin/turbo-golo .` | ||
| 68 | +- **Votre terminal ne gère pas les couleurs 24 bits** : utilisez `turbo-golo -theme turbo-classic`, construit uniquement sur les seize couleurs ANSI. `turbo-dark` et `borland-light` utilisent des couleurs 24 bits. | ||
| 69 | + | ||
| 70 | +## Quand quelque chose ne va pas | ||
| 71 | + | ||
| 72 | +**`the installed binary does not run`.** L'installeur affiche juste au-dessus ce que le système a répondu — lisez-le d'abord, c'est lui qui nomme le vrai problème. | ||
| 73 | + | ||
| 74 | +L'installeur **remplace** le binaire au lieu d'écrire par-dessus celui qui est là : une réinstallation donne donc au fichier une identité neuve. Cela compte sous macOS, qui met en cache la signature de code d'un binaire **par inode** : écrire de nouveaux octets dans l'ancien inode laisse une signature en cache qui décrit autre chose, et le noyau refuse alors d'exécuter un binaire qui s'est pourtant compilé et installé sans erreur. Si une ancienne copie a été installée par un outil qui employait `cp`, la supprimer d'abord efface cet état : | ||
| 75 | + | ||
| 76 | +```bash | ||
| 77 | +scripts/install.sh --uninstall | ||
| 78 | +scripts/install.sh | ||
| 79 | +``` | ||
| 80 | + | ||
| 81 | +**`Go x.y or later is needed`.** La version vient de `go.mod` : elle ne peut donc pas diverger de ce dont le code a réellement besoin. Mettez Go à jour, ou compilez depuis une étiquette correspondant à la chaîne d'outils dont vous disposez. | ||
| 82 | + | ||
| 83 | +**`build failed; nothing was installed`.** Votre installation existante est intacte — la compilation passe d'abord par un fichier temporaire. La sortie du compilateur est affichée au-dessus du message. | ||
| 84 | + | ||
| 85 | +**La barre d'état dit `LSP: no golo — see https://codeberg.org/TypeUnsafe/golo-script/releases`.** L'éditeur fonctionne, mais sans complétion : `golo` n'est ni dans le `PATH` ni dans `/usr/local/bin`. Voir [Installer GoloScript](install-goloscript.md). | ||
| 86 | + | ||
| 87 | +## Exigences côté terminal | ||
| 88 | + | ||
| 89 | +Turbo Golo a besoin d'un terminal qui rapporte sa taille et gère la souris — tous les terminaux courants le font. Il lit `TERM` via tcell ; si l'affichage est incorrect, vérifiez que `TERM` correspond bien à votre terminal (`xterm-256color` est une valeur sûre). | ||
| 90 | + | ||
| 91 | +## Voir aussi | ||
| 92 | + | ||
| 93 | +- Toutes les options : [référence de la ligne de commande](../reference/cli.md) | ||
| 94 | +- Faire marcher la complétion : [Activer la complétion Golo](enable-completion.md) | ||
| 95 | +- Une première session guidée : [Votre premier programme Golo dans Turbo Golo](../tutorials/getting-started.md) | ||
| 96 | +- Des projets pour l'essayer : [les démos](../../../demos/) | ||
added
docs/fr/how-to/make-a-release.md +103 -0 | new file mode 100644 | ||
| @@ -0,0 +1,103 @@ | ||
| 1 | +# Comment faire une release | |
| 2 | + | |
| 3 | +Ce guide montre comment publier une version pour que l'éditeur annonce correctement la sienne. Il suppose que vous pouvez pousser sur le dépôt. | |
| 4 | + | |
| 5 | +## Vérifier ce que vous vous apprêtez à publier | |
| 6 | + | |
| 7 | +```sh | |
| 8 | +make version | |
| 9 | +``` | |
| 10 | + | |
| 11 | +``` | |
| 12 | +v0.1.0-14-g88a4c38 (88a4c38) | |
| 13 | +``` | |
| 14 | + | |
| 15 | +Quatorze commits après `v0.1.0`. Un `-dirty` à la fin signifie que vous avez des modifications non validées — validez-les ou mettez-les de côté d'abord, sinon la release portera ce suffixe pour toujours. | |
| 16 | + | |
| 17 | +## Poser le tag | |
| 18 | + | |
| 19 | +```sh | |
| 20 | +git tag -a v0.2.0 -m "v0.2.0" | |
| 21 | +git push origin v0.2.0 | |
| 22 | +``` | |
| 23 | + | |
| 24 | +Le tag est l'origine du numéro : il doit exister avant de construire quoi que ce soit destiné à être distribué. Annoté (`-a`) plutôt que léger, parce que `git describe` préfère les tags annotés. | |
| 25 | + | |
| 26 | +## Construire le binaire de release | |
| 27 | + | |
| 28 | +```sh | |
| 29 | +make build | |
| 30 | +./bin/turbo-golo -version | |
| 31 | +``` | |
| 32 | + | |
| 33 | +``` | |
| 34 | +Turbo Golo 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z) | |
| 35 | +``` | |
| 36 | + | |
| 37 | +Pas de suffixe `-14-g…` : vous êtes exactement sur le tag. C'est ce qui vous dit que le tag a bien été pris. | |
| 38 | + | |
| 39 | +## Vérifier la boîte About | |
| 40 | + | |
| 41 | +Lancez l'éditeur et faites `Alt-H`, puis `A`. | |
| 42 | + | |
| 43 | +``` | |
| 44 | +Turbo Golo 0.2.0 | |
| 45 | + | |
| 46 | +A Turbo C-style editor for Golo, | |
| 47 | +written in Go. | |
| 48 | + | |
| 49 | +Commit: 88a4c38 | |
| 50 | +Built: 2026-08-31 18:04 UTC | |
| 51 | +Theme: Turbo Classic | |
| 52 | +``` | |
| 53 | + | |
| 54 | +## Ou utiliser les scripts et laisser le workflow publier | |
| 55 | + | |
| 56 | +C'est ainsi qu'une release est réellement faite. Mettez la version et sa description d'une ligne dans `release.env` — il est ignoré par git, donc la CI ne le voit jamais : | |
| 57 | + | |
| 58 | +```sh | |
| 59 | +TAG="v1.0.0" | |
| 60 | +ABOUT="Turbo Golo" | |
| 61 | +``` | |
| 62 | + | |
| 63 | +Puis lancez un seul script : | |
| 64 | + | |
| 65 | +```sh | |
| 66 | +./01-release.tag.sh | |
| 67 | +``` | |
| 68 | + | |
| 69 | +Il lance `make check`, refuse un tag déjà pris en local ou sur `origin`, refuse un `go.mod` portant une directive `replace`, valide ce qui reste à valider, pousse la branche, et seulement ensuite pose le tag et le pousse. Cet ordre compte : un tag poussé avant la branche pointe sur un commit que le distant n'a jamais vu, et un tag créé avant un push refusé reste là pour que quelqu'un le trouve. | |
| 70 | + | |
| 71 | +C'est la dernière chose que vous lancez à la main. Le push du tag déclenche `.github/workflows/release.yml` ; suivez-le dans l'onglet Actions du dépôt. Il lance la suite de tests, compile les binaires avec `./02-build-releases.sh` — le même script que vous pouvez lancer sur votre machine — et crée la page de release avec : le message du tag, la ligne `go install`, les liens vers la documentation **à ce tag**, un binaire par plateforme, le `SHA256SUMS` et le README qui décrit les téléchargements. | |
| 72 | + | |
| 73 | +Le job publie avec son propre `GITHUB_TOKEN`, la seule authentification que l'API de release de Rickub accepte — un jeton personnel est refusé. Il n'y a rien à configurer et aucun secret à garder, ce qui explique la disparition des anciens `02-release.publish.sh` et `04-release.upload-binaries.sh`. | |
| 74 | + | |
| 75 | +`02-build-releases.sh` compile chaque plateforme et **estampille `TAG` lui-même**, en surchargeant la version du Makefile : `make ldflags VERSION=v1.0.0`. La release *est* `v1.0.0`, donc c'est ce que disent ses binaires — quoi qu'aurait répondu `git describe`, et que le tag existe déjà ou non. Il lance ensuite le binaire préparé pour cette machine et vérifie qu'il annonce bien la version : c'est la seule preuve que ce qui est distribué la porte. | |
| 76 | + | |
| 77 | +Vous pouvez voir ce que le workflow publiera sans rien publier, ou construire les binaires à la main : | |
| 78 | + | |
| 79 | +```sh | |
| 80 | +./02-build-releases.sh v1.0.0 # écrit release/v1.0.0/, ne pousse rien | |
| 81 | +``` | |
| 82 | + | |
| 83 | +Sans argument il lit `TAG` dans `release.env` ; le workflow n'a pas de `release.env`, il passe donc le tag qui l'a déclenché. | |
| 84 | + | |
| 85 | +La suite de tests comprend des tests qui exécutent `01-release.tag.sh` contre un clone jetable. Ils se sautent eux-mêmes quand `TURBO_GOLO_RELEASING` est défini, ce que le script exporte avant d'appeler `make check` — retirer cette ligne fait récurser une release jusqu'à épuisement. Le workflow pose la même variable pour sa propre étape `go test`. | |
| 86 | + | |
| 87 | +## Variantes | |
| 88 | + | |
| 89 | +- **Vous installez au lieu de distribuer un binaire.** `make install` et `scripts/install.sh` estampillent de la même façon, donc un éditeur installé nomme le commit dont il vient. Il n'y a rien de plus à faire. | |
| 90 | +- **Quelqu'un installe avec `go install`.** `go install rickub.com/turbo-editors/turbo-golo@v0.2.0` annonce `0.2.0` d'après la version du module, sans commit ni date de build. C'est le propre enregistrement de l'outil Go ; rien à estampiller. | |
| 91 | +- **Vous avez tagué le mauvais commit.** Si le tag n'a pas été poussé, supprimez-le (`git tag -d v1.0.0`), taguez le bon, et reconstruisez. Une fois sur `origin`, ne le déplacez pas : le proxy de modules a mis en cache `go install …@v1.0.0` et la page de release porte déjà des binaires avec ce numéro — c'est pour cela que `01-release.tag.sh` refuse un tag qui existe. Incrémentez `TAG` et refaites une release. | |
| 92 | +- **About affiche `devel`.** Le binaire a été construit par un `go build .` nu plutôt que par `make`. Il n'a rien d'anormal ; simplement aucun tag ne lui a été estampillé, parce que le système de build de Go ne lit pas les tags git. Utilisez `make build`. | |
| 93 | +- **About affiche `unknown`.** Rien n'a nommé le build — un `go run`, ou une construction depuis un dossier sans historique git. Utilisez `make build` depuis le dépôt cloné. | |
| 94 | +- **Vous n'avez pas git du tout**, ayant téléchargé une archive des sources. `make build` fonctionne quand même et le binaire annonce `unknown`. Passez la version vous-même si vous en avez besoin — la variable estampillée est celle du paquet `version` de turbo-core, que tous les éditeurs de la famille partagent : | |
| 95 | + ```sh | |
| 96 | + go build -ldflags "-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0'" -o bin/turbo-golo . | |
| 97 | + ``` | |
| 98 | + | |
| 99 | +## Voir aussi | |
| 100 | + | |
| 101 | +- Toutes les sources du numéro, et ce qu'annonce chaque build : [Le numéro de version](../reference/versioning.md) | |
| 102 | +- Pourquoi il n'y a pas de constante de version dans les sources : [Décisions de conception](../explanation/design-decisions.md#la-version-est-une-propriété-du-build-pas-des-sources) | |
| 103 | +- Installer dans votre PATH : [Comment installer et construire Turbo Golo](install.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,103 @@ | |||
| 1 | +# Comment faire une release | ||
| 2 | + | ||
| 3 | +Ce guide montre comment publier une version pour que l'éditeur annonce correctement la sienne. Il suppose que vous pouvez pousser sur le dépôt. | ||
| 4 | + | ||
| 5 | +## Vérifier ce que vous vous apprêtez à publier | ||
| 6 | + | ||
| 7 | +```sh | ||
| 8 | +make version | ||
| 9 | +``` | ||
| 10 | + | ||
| 11 | +``` | ||
| 12 | +v0.1.0-14-g88a4c38 (88a4c38) | ||
| 13 | +``` | ||
| 14 | + | ||
| 15 | +Quatorze commits après `v0.1.0`. Un `-dirty` à la fin signifie que vous avez des modifications non validées — validez-les ou mettez-les de côté d'abord, sinon la release portera ce suffixe pour toujours. | ||
| 16 | + | ||
| 17 | +## Poser le tag | ||
| 18 | + | ||
| 19 | +```sh | ||
| 20 | +git tag -a v0.2.0 -m "v0.2.0" | ||
| 21 | +git push origin v0.2.0 | ||
| 22 | +``` | ||
| 23 | + | ||
| 24 | +Le tag est l'origine du numéro : il doit exister avant de construire quoi que ce soit destiné à être distribué. Annoté (`-a`) plutôt que léger, parce que `git describe` préfère les tags annotés. | ||
| 25 | + | ||
| 26 | +## Construire le binaire de release | ||
| 27 | + | ||
| 28 | +```sh | ||
| 29 | +make build | ||
| 30 | +./bin/turbo-golo -version | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +``` | ||
| 34 | +Turbo Golo 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z) | ||
| 35 | +``` | ||
| 36 | + | ||
| 37 | +Pas de suffixe `-14-g…` : vous êtes exactement sur le tag. C'est ce qui vous dit que le tag a bien été pris. | ||
| 38 | + | ||
| 39 | +## Vérifier la boîte About | ||
| 40 | + | ||
| 41 | +Lancez l'éditeur et faites `Alt-H`, puis `A`. | ||
| 42 | + | ||
| 43 | +``` | ||
| 44 | +Turbo Golo 0.2.0 | ||
| 45 | + | ||
| 46 | +A Turbo C-style editor for Golo, | ||
| 47 | +written in Go. | ||
| 48 | + | ||
| 49 | +Commit: 88a4c38 | ||
| 50 | +Built: 2026-08-31 18:04 UTC | ||
| 51 | +Theme: Turbo Classic | ||
| 52 | +``` | ||
| 53 | + | ||
| 54 | +## Ou utiliser les scripts et laisser le workflow publier | ||
| 55 | + | ||
| 56 | +C'est ainsi qu'une release est réellement faite. Mettez la version et sa description d'une ligne dans `release.env` — il est ignoré par git, donc la CI ne le voit jamais : | ||
| 57 | + | ||
| 58 | +```sh | ||
| 59 | +TAG="v1.0.0" | ||
| 60 | +ABOUT="Turbo Golo" | ||
| 61 | +``` | ||
| 62 | + | ||
| 63 | +Puis lancez un seul script : | ||
| 64 | + | ||
| 65 | +```sh | ||
| 66 | +./01-release.tag.sh | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +Il lance `make check`, refuse un tag déjà pris en local ou sur `origin`, refuse un `go.mod` portant une directive `replace`, valide ce qui reste à valider, pousse la branche, et seulement ensuite pose le tag et le pousse. Cet ordre compte : un tag poussé avant la branche pointe sur un commit que le distant n'a jamais vu, et un tag créé avant un push refusé reste là pour que quelqu'un le trouve. | ||
| 70 | + | ||
| 71 | +C'est la dernière chose que vous lancez à la main. Le push du tag déclenche `.github/workflows/release.yml` ; suivez-le dans l'onglet Actions du dépôt. Il lance la suite de tests, compile les binaires avec `./02-build-releases.sh` — le même script que vous pouvez lancer sur votre machine — et crée la page de release avec : le message du tag, la ligne `go install`, les liens vers la documentation **à ce tag**, un binaire par plateforme, le `SHA256SUMS` et le README qui décrit les téléchargements. | ||
| 72 | + | ||
| 73 | +Le job publie avec son propre `GITHUB_TOKEN`, la seule authentification que l'API de release de Rickub accepte — un jeton personnel est refusé. Il n'y a rien à configurer et aucun secret à garder, ce qui explique la disparition des anciens `02-release.publish.sh` et `04-release.upload-binaries.sh`. | ||
| 74 | + | ||
| 75 | +`02-build-releases.sh` compile chaque plateforme et **estampille `TAG` lui-même**, en surchargeant la version du Makefile : `make ldflags VERSION=v1.0.0`. La release *est* `v1.0.0`, donc c'est ce que disent ses binaires — quoi qu'aurait répondu `git describe`, et que le tag existe déjà ou non. Il lance ensuite le binaire préparé pour cette machine et vérifie qu'il annonce bien la version : c'est la seule preuve que ce qui est distribué la porte. | ||
| 76 | + | ||
| 77 | +Vous pouvez voir ce que le workflow publiera sans rien publier, ou construire les binaires à la main : | ||
| 78 | + | ||
| 79 | +```sh | ||
| 80 | +./02-build-releases.sh v1.0.0 # écrit release/v1.0.0/, ne pousse rien | ||
| 81 | +``` | ||
| 82 | + | ||
| 83 | +Sans argument il lit `TAG` dans `release.env` ; le workflow n'a pas de `release.env`, il passe donc le tag qui l'a déclenché. | ||
| 84 | + | ||
| 85 | +La suite de tests comprend des tests qui exécutent `01-release.tag.sh` contre un clone jetable. Ils se sautent eux-mêmes quand `TURBO_GOLO_RELEASING` est défini, ce que le script exporte avant d'appeler `make check` — retirer cette ligne fait récurser une release jusqu'à épuisement. Le workflow pose la même variable pour sa propre étape `go test`. | ||
| 86 | + | ||
| 87 | +## Variantes | ||
| 88 | + | ||
| 89 | +- **Vous installez au lieu de distribuer un binaire.** `make install` et `scripts/install.sh` estampillent de la même façon, donc un éditeur installé nomme le commit dont il vient. Il n'y a rien de plus à faire. | ||
| 90 | +- **Quelqu'un installe avec `go install`.** `go install rickub.com/turbo-editors/turbo-golo@v0.2.0` annonce `0.2.0` d'après la version du module, sans commit ni date de build. C'est le propre enregistrement de l'outil Go ; rien à estampiller. | ||
| 91 | +- **Vous avez tagué le mauvais commit.** Si le tag n'a pas été poussé, supprimez-le (`git tag -d v1.0.0`), taguez le bon, et reconstruisez. Une fois sur `origin`, ne le déplacez pas : le proxy de modules a mis en cache `go install …@v1.0.0` et la page de release porte déjà des binaires avec ce numéro — c'est pour cela que `01-release.tag.sh` refuse un tag qui existe. Incrémentez `TAG` et refaites une release. | ||
| 92 | +- **About affiche `devel`.** Le binaire a été construit par un `go build .` nu plutôt que par `make`. Il n'a rien d'anormal ; simplement aucun tag ne lui a été estampillé, parce que le système de build de Go ne lit pas les tags git. Utilisez `make build`. | ||
| 93 | +- **About affiche `unknown`.** Rien n'a nommé le build — un `go run`, ou une construction depuis un dossier sans historique git. Utilisez `make build` depuis le dépôt cloné. | ||
| 94 | +- **Vous n'avez pas git du tout**, ayant téléchargé une archive des sources. `make build` fonctionne quand même et le binaire annonce `unknown`. Passez la version vous-même si vous en avez besoin — la variable estampillée est celle du paquet `version` de turbo-core, que tous les éditeurs de la famille partagent : | ||
| 95 | + ```sh | ||
| 96 | + go build -ldflags "-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0'" -o bin/turbo-golo . | ||
| 97 | + ``` | ||
| 98 | + | ||
| 99 | +## Voir aussi | ||
| 100 | + | ||
| 101 | +- Toutes les sources du numéro, et ce qu'annonce chaque build : [Le numéro de version](../reference/versioning.md) | ||
| 102 | +- Pourquoi il n'y a pas de constante de version dans les sources : [Décisions de conception](../explanation/design-decisions.md#la-version-est-une-propriété-du-build-pas-des-sources) | ||
| 103 | +- Installer dans votre PATH : [Comment installer et construire Turbo Golo](install.md) | ||
added
docs/fr/how-to/run-golo-commands.md +230 -0 | new file mode 100644 | ||
| @@ -0,0 +1,230 @@ | ||
| 1 | +# Lancer des commandes Golo depuis l'éditeur | |
| 2 | + | |
| 3 | +Ce guide montre comment exécuter, tester, déboguer et compiler vos scripts sans quitter Turbo Golo. Il suppose que l'éditeur est installé et que vous avez un répertoire contenant un fichier `.golo`. | |
| 4 | + | |
| 5 | +## Obtenir un fichier de départ | |
| 6 | + | |
| 7 | +Lancez l'éditeur **depuis le répertoire où vivent vos scripts**, puis choisissez **Golo ▸ Create tools file** (`Alt-G`, puis `C`). | |
| 8 | + | |
| 9 | +Cela écrit `.turbo-golo/tools.toml` avec les commandes qu'un programmeur Golo lance le plus, et l'ouvre. Les trois premières : | |
| 10 | + | |
| 11 | +```toml | |
| 12 | +[[tool]] | |
| 13 | +name = "~R~un" | |
| 14 | +# The interpreter, on the file you name. A terminal, not a popup: a script that | |
| 15 | +# reads the keyboard has to be able to be answered, and one that runs long has | |
| 16 | +# to be able to be interrupted. | |
| 17 | +command = "golo {{script, e.g. main.golo}}" | |
| 18 | +output = "terminal" | |
| 19 | + | |
| 20 | +[[tool]] | |
| 21 | +name = "~T~est" | |
| 22 | +# Every *_test.golo under the current directory, with gololang.Testing. | |
| 23 | +command = "golo --test" | |
| 24 | +output = "popup" | |
| 25 | + | |
| 26 | +[[tool]] | |
| 27 | +name = "Test ~o~ne" | |
| 28 | +command = "golo --test {{test file or directory}}" | |
| 29 | +output = "popup" | |
| 30 | +``` | |
| 31 | + | |
| 32 | +Chaque `[[tool]]` devient une ligne du menu **Golo**, dans l'ordre d'apparition — sauf s'il nomme un `menu` à lui, ce qu'une section plus bas couvre. Le fichier est lu à chaque ouverture du menu, une modification prend donc effet immédiatement. | |
| 33 | + | |
| 34 | +## En lancer une | |
| 35 | + | |
| 36 | +`Alt-G`, puis la lettre entre les tildes — `R` pour exécuter, `T` pour tester. | |
| 37 | + | |
| 38 | +**Run** demande d'abord, parce que Golo n'a pas de manifeste qui dise quel fichier est le programme : | |
| 39 | + | |
| 40 | +``` | |
| 41 | +┌──────────────── Run ────────────────┐ | |
| 42 | +│ script, e.g. main.golo │ | |
| 43 | +│ [ ] │ | |
| 44 | +└─────────────────────────────────────┘ | |
| 45 | +``` | |
| 46 | + | |
| 47 | +Tapez `main.golo` et appuyez sur `Entrée`. Une fenêtre de terminal s'ouvre et le script s'y exécute ; quand il finit la fenêtre reste, montrant ce qu'il a imprimé. Appuyez sur `Ctrl-W` pour la fermer. Relancez-le et la boîte se souvient du nom pour le reste de la session. | |
| 48 | + | |
| 49 | +**Test** ouvre aussitôt un **popup**, qui se remplit pendant que la commande tourne. Son titre porte la commande et, une fois finie, comment cela s'est passé : | |
| 50 | + | |
| 51 | +``` | |
| 52 | +┌──────────────── golo --test — ok ─────────────────┐ | |
| 53 | +│ 🧪 Running Golo tests... │ | |
| 54 | +│ │ | |
| 55 | +│ 📝 shapes_test.golo │ | |
| 56 | +│ ✓ a point describes itself │ | |
| 57 | +│ ✅ 1 test(s) passed │ | |
| 58 | +│ │ | |
| 59 | +│ [ Close ] │ | |
| 60 | +└────────────────────────────────────────────────────┘ | |
| 61 | +``` | |
| 62 | + | |
| 63 | +| Touche | Effet | | |
| 64 | +| --- | --- | | |
| 65 | +| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Parcourir la sortie | | |
| 66 | +| `Échap` | Le fermer — et **arrêter la commande** si elle tourne encore | | |
| 67 | +| `Entrée` | Le fermer | | |
| 68 | + | |
| 69 | +Une commande qui a réussi en silence affiche `(no output)` plutôt qu'une boîte vide, pour qu'on la distingue d'une commande qui n'a pas démarré. | |
| 70 | + | |
| 71 | +## Le reste du menu | |
| 72 | + | |
| 73 | +| Entrée | Ce qu'elle lance | Où | | |
| 74 | +| --- | --- | --- | | |
| 75 | +| **Debug** | `golo --debug <script>` — l'interpréteur avec son débogueur pas à pas | un terminal, parce que le débogueur lit le clavier | | |
| 76 | +| **REPL** | `golo` — la boucle lire-évaluer-imprimer | un terminal | | |
| 77 | +| **New script** | `golo new main --module <nom> --name <fichier>` — un programme de départ tiré du modèle de GoloScript | un popup ; le nouveau fichier apparaît dans l'arbre du projet | | |
| 78 | +| **Build native** | `gogolo build -o <sortie> <script>` — Golo → Go → un exécutable natif | un popup ; il faut l'outillage Go | | |
| 79 | +| **Build wasm** | `wagolo build -target=<wasi\|js\|wasip2> -o <sortie.wasm> <script>` — Golo → Go → TinyGo → WebAssembly | un popup ; il faut TinyGo, et `wasm-tools` pour `wasip2` | | |
| 80 | + | |
| 81 | +`Build native` peut prendre du temps : il lance le compilateur Go. Le popup est modal, pendant qu'il tourne vous ne pouvez taper nulle part ailleurs ; `Échap` le ferme et arrête la compilation. | |
| 82 | + | |
| 83 | +## Choisir où va la sortie | |
| 84 | + | |
| 85 | +Réglez `output` sur un outil : | |
| 86 | + | |
| 87 | +| `output` | Ce que vous obtenez | | |
| 88 | +| --- | --- | | |
| 89 | +| `popup` | Un dialogue qui se remplit pendant l'exécution. Le défaut. | | |
| 90 | +| `terminal` | Une fenêtre de terminal : les couleurs, `Ctrl-C`, et le clavier atteint le programme | | |
| 91 | +| `editor` | Une fenêtre d'édition une fois fini, pour fouiller avec `Ctrl-F` | | |
| 92 | + | |
| 93 | +`Run`, `Debug` et `REPL` sont en `terminal` dans le fichier de départ, et ils sont l'exemple de la raison d'être de la clé : un popup ne peut pas répondre à un script qui appelle `readln`, ne peut pas être interrompu par `Ctrl-C` pendant qu'`httpServe` écoute, et ne peut pas du tout être un REPL. | |
| 94 | + | |
| 95 | +Prenez `editor` quand la sortie est quelque chose à parcourir — le source Go que `gogolo transpile main.golo` imprime, ou un long rapport de tests que vous voulez fouiller. | |
| 96 | + | |
| 97 | +## Une commande longue retient l'éditeur | |
| 98 | + | |
| 99 | +Un popup est modal : pendant que `gogolo build` tourne, vous ne pouvez taper nulle part ailleurs. `Échap` le ferme et arrête la commande. | |
| 100 | + | |
| 101 | +Si cela gêne pour une commande donnée, donnez-lui `output = "terminal"` — la fenêtre est une fenêtre ordinaire et vous pouvez continuer à travailler à côté. C'est à cela que sert la clé configurable. | |
| 102 | + | |
| 103 | +## Ce qui arrive à vos fichiers ouverts | |
| 104 | + | |
| 105 | +Golo n'a pas de formateur, rien dans le fichier de départ ne réécrit donc le fichier que vous regardez. Mais `New script` écrit un nouveau fichier dans le répertoire, `gogolo build -keep-go` laisse un `.go` à côté de votre script, et un outil à vous peut faire n'importe quoi. Quand une commande se termine, l'éditeur **relit tous les fichiers ouverts qui n'ont pas de modifications non enregistrées**, un fichier qu'une autre commande a changé apparaît donc tel qu'il est désormais, et l'arbre du projet est rafraîchi pour qu'un nouveau fichier s'y montre. La barre d'état dit combien. | |
| 106 | + | |
| 107 | +Un fichier avec des modifications non enregistrées est **laissé tranquille**, et la barre d'état le dit aussi : | |
| 108 | + | |
| 109 | +``` | |
| 110 | +Reloaded 2 files; 1 file with unsaved changes left alone | |
| 111 | +``` | |
| 112 | + | |
| 113 | +C'est délibéré : votre modification et la commande sont réellement en désaccord, et ce n'est pas à l'éditeur de décider qui l'emporte. Enregistrez d'abord (`F2`) et relancez la commande, ou continuez à éditer. | |
| 114 | + | |
| 115 | +## Ajouter vos propres commandes | |
| 116 | + | |
| 117 | +Éditez `.turbo-golo/tools.toml`. Une commande va à `sh -c`, une entrée peut donc être une séquence entière : | |
| 118 | + | |
| 119 | +```toml | |
| 120 | +[[tool]] | |
| 121 | +name = "Test and ~b~uild" | |
| 122 | +command = "golo --test && gogolo build -o bin/app main.golo" | |
| 123 | +output = "popup" | |
| 124 | + | |
| 125 | +[[tool]] | |
| 126 | +name = "~T~ranspile" | |
| 127 | +command = "gogolo transpile {{script, e.g. main.golo}}" | |
| 128 | +output = "editor" | |
| 129 | + | |
| 130 | +[[tool]] | |
| 131 | +name = "Run in ~D~ocker" | |
| 132 | +command = "docker run --rm -v \"$PWD:/app\" -w /app k33g/gololang:latest /golo ./{{script}}" | |
| 133 | +output = "terminal" | |
| 134 | +``` | |
| 135 | + | |
| 136 | +Donnez à chacune une touche chaude entre tildes, et gardez-les distinctes — le menu répond à la première correspondance qu'il trouve. | |
| 137 | + | |
| 138 | +## Mettre un outil dans un menu à lui | |
| 139 | + | |
| 140 | +Un outil qui n'a rien à voir avec Golo n'a pas sa place dans le menu Golo. Donnez-lui un `menu` : | |
| 141 | + | |
| 142 | +```toml | |
| 143 | +[[tool]] | |
| 144 | +name = "~E~cho" | |
| 145 | +command = "echo TADA" | |
| 146 | +output = "terminal" | |
| 147 | +menu = "Tools" | |
| 148 | + | |
| 149 | +[[tool]] | |
| 150 | +name = "~U~p" | |
| 151 | +command = "docker compose up -d" | |
| 152 | +menu = "Docker" | |
| 153 | + | |
| 154 | +[[tool]] | |
| 155 | +name = "~D~own" | |
| 156 | +command = "docker compose down" | |
| 157 | +menu = "Docker" | |
| 158 | +``` | |
| 159 | + | |
| 160 | +Cela vous donne un menu **Tools** et un menu **Docker** sur la barre, entre Golo et Help, dans l'ordre où les noms apparaissent pour la première fois dans le fichier. Docker contient ses deux outils. Rien à redémarrer : enregistrez le fichier et la barre suit. | |
| 161 | + | |
| 162 | +Le nom est à vous — il n'y a pas de liste où choisir. Omettez `menu` et l'outil reste dans Golo, où sont huit des neuf commandes de départ. | |
| 163 | + | |
| 164 | +### La touche chaude est choisie pour vous | |
| 165 | + | |
| 166 | +Vous ne pouvez pas savoir, en écrivant le fichier, quelles lettres les menus propres de l'éditeur ont prises. Il le calcule donc : la première lettre du nom que rien d'autre ne revendique reçoit les tildes. | |
| 167 | + | |
| 168 | +`Tools` obtient `Alt-T`, parce que `T` est libre. Un menu appelé `Format` obtiendrait `Alt-A`, parce que `F` est à File, `o` à Options et `r` à Run. Un menu appelé `Go` n'obtiendrait aucune touche du tout — `G` est à Golo et `O` à Options — et `F10` serait le chemin vers lui. | |
| 169 | + | |
| 170 | +Écrivez les tildes vous-même — `menu = "Doc~k~er"` — et une lettre libre est conservée. Une lettre prise ne l'est pas : la barre répond au *premier* menu qui correspond à une touche, honorer votre choix rendrait donc l'un des deux menus inatteignable. Il choisit une autre lettre et ne dit rien. | |
| 171 | + | |
| 172 | +## Variantes | |
| 173 | + | |
| 174 | +- **Vous avez un seul script et jamais un autre.** Remplacez `golo {{script, e.g. main.golo}}` par `golo main.golo`, et la boîte cesse d'apparaître. Le champ est là parce qu'un fichier de départ ne peut pas savoir quel fichier est le programme. | |
| 175 | +- **Vous avez lancé l'éditeur depuis un sous-répertoire.** Les commandes s'y exécutent, et les chemins relatifs dans la boîte sont relatifs à lui. Partez du répertoire où sont les scripts. | |
| 176 | +- **Le fichier contient une erreur.** Le menu affiche un `Cannot read tools` grisé à la place des commandes, et **Create tools file** est toujours là. | |
| 177 | +- **`gogolo` ou `wagolo` n'est pas installé.** Le popup affiche `command not found` et `— exit 127`, ce qu'un shell aurait dit. L'`install.sh` de GoloScript installe les trois binaires ensemble ; un téléchargement de release en donne un à la fois. | |
| 178 | +- **Vous voulez un menu du nom d'un qui existe.** `menu = "File"` vous donne un second menu File, plus loin sur la barre, avec une autre touche chaude. Rien ne l'empêche ; rien ne le recommande non plus. | |
| 179 | +- **Votre menu n'a pas de touche chaude.** Toutes les lettres de son nom étaient déjà prises. `F10` et les flèches l'atteignent, et la souris aussi. Renommez-le avec une lettre libre. | |
| 180 | +- **Vous avez mal orthographié la valeur d'`output`.** Tout le fichier est refusé et le menu dit `Cannot read tools`, en nommant l'outil et en listant ce que la valeur aurait pu être. Un repli silencieux aurait envoyé la sortie quelque part où vous ne l'aviez pas demandée. | |
| 181 | + | |
| 182 | +## Demander une valeur à l'exécution | |
| 183 | + | |
| 184 | +Six des commandes de départ le font déjà. Le motif est un `{{libellé}}` là où va la valeur : | |
| 185 | + | |
| 186 | +```toml | |
| 187 | +[[tool]] | |
| 188 | +name = "~N~ew script" | |
| 189 | +command = "golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}" | |
| 190 | +output = "popup" | |
| 191 | +``` | |
| 192 | + | |
| 193 | +La choisir ouvre une boîte titrée **New script** avec deux champs, un par `{{…}}`, dans leur ordre d'apparition. **Tab** passe de l'un à l'autre, **Entrée** lance la commande. Échap, et rien ne se lance. | |
| 194 | + | |
| 195 | +La valeur est citée, un chemin avec un espace reste donc un seul argument. | |
| 196 | + | |
| 197 | +### Un champ pour plusieurs arguments | |
| 198 | + | |
| 199 | +Citer est faux quand on veut dire « mets ceci à la fin ». Ajoutez `...` dans les accolades et la valeur passe telle quelle : | |
| 200 | + | |
| 201 | +```toml | |
| 202 | +[[tool]] | |
| 203 | +name = "Run with ~a~rguments" | |
| 204 | +command = "golo main.golo {{arguments...}}" | |
| 205 | +output = "terminal" | |
| 206 | +``` | |
| 207 | + | |
| 208 | +Tapez `--verbose input.txt` et les deux atteignent le script comme des arguments séparés — `args` dans `function main = |args|` les contient. | |
| 209 | + | |
| 210 | +### La même valeur deux fois | |
| 211 | + | |
| 212 | +Écrivez le libellé deux fois ; on vous la demande une fois : | |
| 213 | + | |
| 214 | +```toml | |
| 215 | +[[tool]] | |
| 216 | +name = "~C~ompile and run" | |
| 217 | +command = "gogolo build -o /tmp/app {{script}} && /tmp/app" | |
| 218 | +``` | |
| 219 | + | |
| 220 | +### Variantes | |
| 221 | + | |
| 222 | +- **La valeur est la même la plupart du temps.** Lancez une fois et la boîte se souvient de ce que vous avez tapé, pour le reste de la session. Ce n'est pas écrit sur le disque. | |
| 223 | +- **Votre commande a déjà des accolades.** `awk '{print $1}'` et `find . -exec rm {} +` sont laissés tranquilles : seules les doubles accolades demandent quelque chose. | |
| 224 | +- **La commande demande plus de valeurs qu'il n'en tient à l'écran.** L'éditeur le dit plutôt que d'ouvrir une boîte dont le bouton OK est sous le bas du terminal. Agrandissez le terminal, ou coupez la commande en deux outils. | |
| 225 | + | |
| 226 | +## Voir aussi | |
| 227 | + | |
| 228 | +- Chaque clé du fichier et chaque règle : [Référence des outils Golo](../reference/golo-tools.md) | |
| 229 | +- Pourquoi Run vient en premier, et pourquoi un fichier non modifié se recharge : [Outils Golo](../explanation/golo-tools.md) | |
| 230 | +- Les fenêtres dans lesquelles les commandes tournent : [Fenêtres de terminal](../reference/terminal.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,230 @@ | |||
| 1 | +# Lancer des commandes Golo depuis l'éditeur | ||
| 2 | + | ||
| 3 | +Ce guide montre comment exécuter, tester, déboguer et compiler vos scripts sans quitter Turbo Golo. Il suppose que l'éditeur est installé et que vous avez un répertoire contenant un fichier `.golo`. | ||
| 4 | + | ||
| 5 | +## Obtenir un fichier de départ | ||
| 6 | + | ||
| 7 | +Lancez l'éditeur **depuis le répertoire où vivent vos scripts**, puis choisissez **Golo ▸ Create tools file** (`Alt-G`, puis `C`). | ||
| 8 | + | ||
| 9 | +Cela écrit `.turbo-golo/tools.toml` avec les commandes qu'un programmeur Golo lance le plus, et l'ouvre. Les trois premières : | ||
| 10 | + | ||
| 11 | +```toml | ||
| 12 | +[[tool]] | ||
| 13 | +name = "~R~un" | ||
| 14 | +# The interpreter, on the file you name. A terminal, not a popup: a script that | ||
| 15 | +# reads the keyboard has to be able to be answered, and one that runs long has | ||
| 16 | +# to be able to be interrupted. | ||
| 17 | +command = "golo {{script, e.g. main.golo}}" | ||
| 18 | +output = "terminal" | ||
| 19 | + | ||
| 20 | +[[tool]] | ||
| 21 | +name = "~T~est" | ||
| 22 | +# Every *_test.golo under the current directory, with gololang.Testing. | ||
| 23 | +command = "golo --test" | ||
| 24 | +output = "popup" | ||
| 25 | + | ||
| 26 | +[[tool]] | ||
| 27 | +name = "Test ~o~ne" | ||
| 28 | +command = "golo --test {{test file or directory}}" | ||
| 29 | +output = "popup" | ||
| 30 | +``` | ||
| 31 | + | ||
| 32 | +Chaque `[[tool]]` devient une ligne du menu **Golo**, dans l'ordre d'apparition — sauf s'il nomme un `menu` à lui, ce qu'une section plus bas couvre. Le fichier est lu à chaque ouverture du menu, une modification prend donc effet immédiatement. | ||
| 33 | + | ||
| 34 | +## En lancer une | ||
| 35 | + | ||
| 36 | +`Alt-G`, puis la lettre entre les tildes — `R` pour exécuter, `T` pour tester. | ||
| 37 | + | ||
| 38 | +**Run** demande d'abord, parce que Golo n'a pas de manifeste qui dise quel fichier est le programme : | ||
| 39 | + | ||
| 40 | +``` | ||
| 41 | +┌──────────────── Run ────────────────┐ | ||
| 42 | +│ script, e.g. main.golo │ | ||
| 43 | +│ [ ] │ | ||
| 44 | +└─────────────────────────────────────┘ | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +Tapez `main.golo` et appuyez sur `Entrée`. Une fenêtre de terminal s'ouvre et le script s'y exécute ; quand il finit la fenêtre reste, montrant ce qu'il a imprimé. Appuyez sur `Ctrl-W` pour la fermer. Relancez-le et la boîte se souvient du nom pour le reste de la session. | ||
| 48 | + | ||
| 49 | +**Test** ouvre aussitôt un **popup**, qui se remplit pendant que la commande tourne. Son titre porte la commande et, une fois finie, comment cela s'est passé : | ||
| 50 | + | ||
| 51 | +``` | ||
| 52 | +┌──────────────── golo --test — ok ─────────────────┐ | ||
| 53 | +│ 🧪 Running Golo tests... │ | ||
| 54 | +│ │ | ||
| 55 | +│ 📝 shapes_test.golo │ | ||
| 56 | +│ ✓ a point describes itself │ | ||
| 57 | +│ ✅ 1 test(s) passed │ | ||
| 58 | +│ │ | ||
| 59 | +│ [ Close ] │ | ||
| 60 | +└────────────────────────────────────────────────────┘ | ||
| 61 | +``` | ||
| 62 | + | ||
| 63 | +| Touche | Effet | | ||
| 64 | +| --- | --- | | ||
| 65 | +| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Parcourir la sortie | | ||
| 66 | +| `Échap` | Le fermer — et **arrêter la commande** si elle tourne encore | | ||
| 67 | +| `Entrée` | Le fermer | | ||
| 68 | + | ||
| 69 | +Une commande qui a réussi en silence affiche `(no output)` plutôt qu'une boîte vide, pour qu'on la distingue d'une commande qui n'a pas démarré. | ||
| 70 | + | ||
| 71 | +## Le reste du menu | ||
| 72 | + | ||
| 73 | +| Entrée | Ce qu'elle lance | Où | | ||
| 74 | +| --- | --- | --- | | ||
| 75 | +| **Debug** | `golo --debug <script>` — l'interpréteur avec son débogueur pas à pas | un terminal, parce que le débogueur lit le clavier | | ||
| 76 | +| **REPL** | `golo` — la boucle lire-évaluer-imprimer | un terminal | | ||
| 77 | +| **New script** | `golo new main --module <nom> --name <fichier>` — un programme de départ tiré du modèle de GoloScript | un popup ; le nouveau fichier apparaît dans l'arbre du projet | | ||
| 78 | +| **Build native** | `gogolo build -o <sortie> <script>` — Golo → Go → un exécutable natif | un popup ; il faut l'outillage Go | | ||
| 79 | +| **Build wasm** | `wagolo build -target=<wasi\|js\|wasip2> -o <sortie.wasm> <script>` — Golo → Go → TinyGo → WebAssembly | un popup ; il faut TinyGo, et `wasm-tools` pour `wasip2` | | ||
| 80 | + | ||
| 81 | +`Build native` peut prendre du temps : il lance le compilateur Go. Le popup est modal, pendant qu'il tourne vous ne pouvez taper nulle part ailleurs ; `Échap` le ferme et arrête la compilation. | ||
| 82 | + | ||
| 83 | +## Choisir où va la sortie | ||
| 84 | + | ||
| 85 | +Réglez `output` sur un outil : | ||
| 86 | + | ||
| 87 | +| `output` | Ce que vous obtenez | | ||
| 88 | +| --- | --- | | ||
| 89 | +| `popup` | Un dialogue qui se remplit pendant l'exécution. Le défaut. | | ||
| 90 | +| `terminal` | Une fenêtre de terminal : les couleurs, `Ctrl-C`, et le clavier atteint le programme | | ||
| 91 | +| `editor` | Une fenêtre d'édition une fois fini, pour fouiller avec `Ctrl-F` | | ||
| 92 | + | ||
| 93 | +`Run`, `Debug` et `REPL` sont en `terminal` dans le fichier de départ, et ils sont l'exemple de la raison d'être de la clé : un popup ne peut pas répondre à un script qui appelle `readln`, ne peut pas être interrompu par `Ctrl-C` pendant qu'`httpServe` écoute, et ne peut pas du tout être un REPL. | ||
| 94 | + | ||
| 95 | +Prenez `editor` quand la sortie est quelque chose à parcourir — le source Go que `gogolo transpile main.golo` imprime, ou un long rapport de tests que vous voulez fouiller. | ||
| 96 | + | ||
| 97 | +## Une commande longue retient l'éditeur | ||
| 98 | + | ||
| 99 | +Un popup est modal : pendant que `gogolo build` tourne, vous ne pouvez taper nulle part ailleurs. `Échap` le ferme et arrête la commande. | ||
| 100 | + | ||
| 101 | +Si cela gêne pour une commande donnée, donnez-lui `output = "terminal"` — la fenêtre est une fenêtre ordinaire et vous pouvez continuer à travailler à côté. C'est à cela que sert la clé configurable. | ||
| 102 | + | ||
| 103 | +## Ce qui arrive à vos fichiers ouverts | ||
| 104 | + | ||
| 105 | +Golo n'a pas de formateur, rien dans le fichier de départ ne réécrit donc le fichier que vous regardez. Mais `New script` écrit un nouveau fichier dans le répertoire, `gogolo build -keep-go` laisse un `.go` à côté de votre script, et un outil à vous peut faire n'importe quoi. Quand une commande se termine, l'éditeur **relit tous les fichiers ouverts qui n'ont pas de modifications non enregistrées**, un fichier qu'une autre commande a changé apparaît donc tel qu'il est désormais, et l'arbre du projet est rafraîchi pour qu'un nouveau fichier s'y montre. La barre d'état dit combien. | ||
| 106 | + | ||
| 107 | +Un fichier avec des modifications non enregistrées est **laissé tranquille**, et la barre d'état le dit aussi : | ||
| 108 | + | ||
| 109 | +``` | ||
| 110 | +Reloaded 2 files; 1 file with unsaved changes left alone | ||
| 111 | +``` | ||
| 112 | + | ||
| 113 | +C'est délibéré : votre modification et la commande sont réellement en désaccord, et ce n'est pas à l'éditeur de décider qui l'emporte. Enregistrez d'abord (`F2`) et relancez la commande, ou continuez à éditer. | ||
| 114 | + | ||
| 115 | +## Ajouter vos propres commandes | ||
| 116 | + | ||
| 117 | +Éditez `.turbo-golo/tools.toml`. Une commande va à `sh -c`, une entrée peut donc être une séquence entière : | ||
| 118 | + | ||
| 119 | +```toml | ||
| 120 | +[[tool]] | ||
| 121 | +name = "Test and ~b~uild" | ||
| 122 | +command = "golo --test && gogolo build -o bin/app main.golo" | ||
| 123 | +output = "popup" | ||
| 124 | + | ||
| 125 | +[[tool]] | ||
| 126 | +name = "~T~ranspile" | ||
| 127 | +command = "gogolo transpile {{script, e.g. main.golo}}" | ||
| 128 | +output = "editor" | ||
| 129 | + | ||
| 130 | +[[tool]] | ||
| 131 | +name = "Run in ~D~ocker" | ||
| 132 | +command = "docker run --rm -v \"$PWD:/app\" -w /app k33g/gololang:latest /golo ./{{script}}" | ||
| 133 | +output = "terminal" | ||
| 134 | +``` | ||
| 135 | + | ||
| 136 | +Donnez à chacune une touche chaude entre tildes, et gardez-les distinctes — le menu répond à la première correspondance qu'il trouve. | ||
| 137 | + | ||
| 138 | +## Mettre un outil dans un menu à lui | ||
| 139 | + | ||
| 140 | +Un outil qui n'a rien à voir avec Golo n'a pas sa place dans le menu Golo. Donnez-lui un `menu` : | ||
| 141 | + | ||
| 142 | +```toml | ||
| 143 | +[[tool]] | ||
| 144 | +name = "~E~cho" | ||
| 145 | +command = "echo TADA" | ||
| 146 | +output = "terminal" | ||
| 147 | +menu = "Tools" | ||
| 148 | + | ||
| 149 | +[[tool]] | ||
| 150 | +name = "~U~p" | ||
| 151 | +command = "docker compose up -d" | ||
| 152 | +menu = "Docker" | ||
| 153 | + | ||
| 154 | +[[tool]] | ||
| 155 | +name = "~D~own" | ||
| 156 | +command = "docker compose down" | ||
| 157 | +menu = "Docker" | ||
| 158 | +``` | ||
| 159 | + | ||
| 160 | +Cela vous donne un menu **Tools** et un menu **Docker** sur la barre, entre Golo et Help, dans l'ordre où les noms apparaissent pour la première fois dans le fichier. Docker contient ses deux outils. Rien à redémarrer : enregistrez le fichier et la barre suit. | ||
| 161 | + | ||
| 162 | +Le nom est à vous — il n'y a pas de liste où choisir. Omettez `menu` et l'outil reste dans Golo, où sont huit des neuf commandes de départ. | ||
| 163 | + | ||
| 164 | +### La touche chaude est choisie pour vous | ||
| 165 | + | ||
| 166 | +Vous ne pouvez pas savoir, en écrivant le fichier, quelles lettres les menus propres de l'éditeur ont prises. Il le calcule donc : la première lettre du nom que rien d'autre ne revendique reçoit les tildes. | ||
| 167 | + | ||
| 168 | +`Tools` obtient `Alt-T`, parce que `T` est libre. Un menu appelé `Format` obtiendrait `Alt-A`, parce que `F` est à File, `o` à Options et `r` à Run. Un menu appelé `Go` n'obtiendrait aucune touche du tout — `G` est à Golo et `O` à Options — et `F10` serait le chemin vers lui. | ||
| 169 | + | ||
| 170 | +Écrivez les tildes vous-même — `menu = "Doc~k~er"` — et une lettre libre est conservée. Une lettre prise ne l'est pas : la barre répond au *premier* menu qui correspond à une touche, honorer votre choix rendrait donc l'un des deux menus inatteignable. Il choisit une autre lettre et ne dit rien. | ||
| 171 | + | ||
| 172 | +## Variantes | ||
| 173 | + | ||
| 174 | +- **Vous avez un seul script et jamais un autre.** Remplacez `golo {{script, e.g. main.golo}}` par `golo main.golo`, et la boîte cesse d'apparaître. Le champ est là parce qu'un fichier de départ ne peut pas savoir quel fichier est le programme. | ||
| 175 | +- **Vous avez lancé l'éditeur depuis un sous-répertoire.** Les commandes s'y exécutent, et les chemins relatifs dans la boîte sont relatifs à lui. Partez du répertoire où sont les scripts. | ||
| 176 | +- **Le fichier contient une erreur.** Le menu affiche un `Cannot read tools` grisé à la place des commandes, et **Create tools file** est toujours là. | ||
| 177 | +- **`gogolo` ou `wagolo` n'est pas installé.** Le popup affiche `command not found` et `— exit 127`, ce qu'un shell aurait dit. L'`install.sh` de GoloScript installe les trois binaires ensemble ; un téléchargement de release en donne un à la fois. | ||
| 178 | +- **Vous voulez un menu du nom d'un qui existe.** `menu = "File"` vous donne un second menu File, plus loin sur la barre, avec une autre touche chaude. Rien ne l'empêche ; rien ne le recommande non plus. | ||
| 179 | +- **Votre menu n'a pas de touche chaude.** Toutes les lettres de son nom étaient déjà prises. `F10` et les flèches l'atteignent, et la souris aussi. Renommez-le avec une lettre libre. | ||
| 180 | +- **Vous avez mal orthographié la valeur d'`output`.** Tout le fichier est refusé et le menu dit `Cannot read tools`, en nommant l'outil et en listant ce que la valeur aurait pu être. Un repli silencieux aurait envoyé la sortie quelque part où vous ne l'aviez pas demandée. | ||
| 181 | + | ||
| 182 | +## Demander une valeur à l'exécution | ||
| 183 | + | ||
| 184 | +Six des commandes de départ le font déjà. Le motif est un `{{libellé}}` là où va la valeur : | ||
| 185 | + | ||
| 186 | +```toml | ||
| 187 | +[[tool]] | ||
| 188 | +name = "~N~ew script" | ||
| 189 | +command = "golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}" | ||
| 190 | +output = "popup" | ||
| 191 | +``` | ||
| 192 | + | ||
| 193 | +La choisir ouvre une boîte titrée **New script** avec deux champs, un par `{{…}}`, dans leur ordre d'apparition. **Tab** passe de l'un à l'autre, **Entrée** lance la commande. Échap, et rien ne se lance. | ||
| 194 | + | ||
| 195 | +La valeur est citée, un chemin avec un espace reste donc un seul argument. | ||
| 196 | + | ||
| 197 | +### Un champ pour plusieurs arguments | ||
| 198 | + | ||
| 199 | +Citer est faux quand on veut dire « mets ceci à la fin ». Ajoutez `...` dans les accolades et la valeur passe telle quelle : | ||
| 200 | + | ||
| 201 | +```toml | ||
| 202 | +[[tool]] | ||
| 203 | +name = "Run with ~a~rguments" | ||
| 204 | +command = "golo main.golo {{arguments...}}" | ||
| 205 | +output = "terminal" | ||
| 206 | +``` | ||
| 207 | + | ||
| 208 | +Tapez `--verbose input.txt` et les deux atteignent le script comme des arguments séparés — `args` dans `function main = |args|` les contient. | ||
| 209 | + | ||
| 210 | +### La même valeur deux fois | ||
| 211 | + | ||
| 212 | +Écrivez le libellé deux fois ; on vous la demande une fois : | ||
| 213 | + | ||
| 214 | +```toml | ||
| 215 | +[[tool]] | ||
| 216 | +name = "~C~ompile and run" | ||
| 217 | +command = "gogolo build -o /tmp/app {{script}} && /tmp/app" | ||
| 218 | +``` | ||
| 219 | + | ||
| 220 | +### Variantes | ||
| 221 | + | ||
| 222 | +- **La valeur est la même la plupart du temps.** Lancez une fois et la boîte se souvient de ce que vous avez tapé, pour le reste de la session. Ce n'est pas écrit sur le disque. | ||
| 223 | +- **Votre commande a déjà des accolades.** `awk '{print $1}'` et `find . -exec rm {} +` sont laissés tranquilles : seules les doubles accolades demandent quelque chose. | ||
| 224 | +- **La commande demande plus de valeurs qu'il n'en tient à l'écran.** L'éditeur le dit plutôt que d'ouvrir une boîte dont le bouton OK est sous le bas du terminal. Agrandissez le terminal, ou coupez la commande en deux outils. | ||
| 225 | + | ||
| 226 | +## Voir aussi | ||
| 227 | + | ||
| 228 | +- Chaque clé du fichier et chaque règle : [Référence des outils Golo](../reference/golo-tools.md) | ||
| 229 | +- Pourquoi Run vient en premier, et pourquoi un fichier non modifié se recharge : [Outils Golo](../explanation/golo-tools.md) | ||
| 230 | +- Les fenêtres dans lesquelles les commandes tournent : [Fenêtres de terminal](../reference/terminal.md) | ||
added
docs/fr/how-to/run-the-tests.md +97 -0 | new file mode 100644 | ||
| @@ -0,0 +1,97 @@ | ||
| 1 | +# Lancer les tests | |
| 2 | + | |
| 3 | +Ce guide montre comment exécuter et lire la suite de tests de Turbo Golo. Il suppose que vous avez un clone du dépôt et Go 1.26 ou plus récent. | |
| 4 | + | |
| 5 | +## Toute la suite | |
| 6 | + | |
| 7 | +```bash | |
| 8 | +make test | |
| 9 | +``` | |
| 10 | + | |
| 11 | +C'est la commande unique documentée. Elle exécute `go test ./...` sur tous les paquets. | |
| 12 | + | |
| 13 | +## Variantes | |
| 14 | + | |
| 15 | +**Voir chaque test par son nom :** | |
| 16 | + | |
| 17 | +```bash | |
| 18 | +make test-verbose | |
| 19 | +``` | |
| 20 | + | |
| 21 | +**Mesurer la couverture par paquet :** | |
| 22 | + | |
| 23 | +```bash | |
| 24 | +make cover | |
| 25 | +``` | |
| 26 | + | |
| 27 | +**Un seul paquet :** | |
| 28 | + | |
| 29 | +```bash | |
| 30 | +go test ./internal/gololang/ | |
| 31 | +``` | |
| 32 | + | |
| 33 | +**Sans lancer de serveur de langage, et sans compiler l'éditeur entier.** Les tests de `internal/gololang` qui portent `WithRealGoloLSP` dans leur nom démarrent un vrai `golo lsp` s'ils en trouvent un, et les tests de l'installeur compilent tout l'éditeur. Pour les sauter : | |
| 34 | + | |
| 35 | +```bash | |
| 36 | +go test -short ./... | |
| 37 | +``` | |
| 38 | + | |
| 39 | +**Tout ce qu'un commit devrait passer :** | |
| 40 | + | |
| 41 | +```bash | |
| 42 | +make check | |
| 43 | +``` | |
| 44 | + | |
| 45 | +Cela enchaîne `go fmt`, `go vet` et les tests, dans cet ordre. | |
| 46 | + | |
| 47 | +## Ce que la suite couvre | |
| 48 | + | |
| 49 | +Aucun test n'a besoin d'un vrai terminal : l'éditeur est dessiné sur le `SimulationScreen` de tcell — un vrai `Screen` qui dessine en mémoire — si bien que les assertions portent sur l'image qu'un terminal afficherait réellement. | |
| 50 | + | |
| 51 | +Ce que ce dépôt teste est ce qui n'appartient qu'à lui : | |
| 52 | + | |
| 53 | +- **Le scanner.** Chaque construction que le scanner colore, et chaque chose qu'il refuse de colorer — `0xFF` n'est pas un nombre, `1_000` non plus — ; une chaîne ou un commentaire `----` portés jusqu'à leur fermeture, sur autant de lignes qu'il faut ; et deux tests qui tiennent la table des mots-clés et celle des fonctions intégrées à ce que le vrai `golo lsp` propose. | |
| 54 | +- **Le profil.** Le nom de l'éditeur, le menu **Golo** et sa touche `Alt-G` qui n'entre en conflit avec aucun menu fixe, l'absence de marqueur de projet, le serveur qui est l'interpréteur en mode `lsp`, et le dossier `/usr/local/bin` où il est cherché après le `PATH`. | |
| 55 | +- **Les fichiers de départ.** Chaque snippet se charge et vise `golo`, les corps sont indentés de deux espaces et écrits en chaînes littérales TOML ; chaque outil se charge et lance `golo`, `gogolo` ou `wagolo` ; deux outils d'un même menu ne se disputent jamais une lettre. | |
| 56 | +- **Le serveur, pour de vrai.** Complétion, déclaration, description d'une fonction avec le commentaire `#` au-dessus d'elle, plan du fichier, références et implémentation d'un appel, recherche d'un symbole dans le projet, diagnostic d'un fichier qui ne s'analyse pas et d'un commentaire à la C — et un test qui vérifie que la définition de type, que `golo lsp` n'annonce pas, répond bien « rien » ; si un futur `golo` l'apprend, ce test le dira, comme il l'a dit quand GoloScript v0.2.0 a appris les références, les implémentations et les symboles du projet. Ces tests **se sautent eux-mêmes** quand `golo` n'est pas installé : un clone sans GoloScript a donc quand même une suite verte. | |
| 57 | +- **L'installeur et la version.** Un test compile l'éditeur par `scripts/install.sh` dans un préfixe temporaire et vérifie qu'il annonce `Turbo Golo` et le commit ; d'autres tiennent `scripts/check-version.sh` et les scripts de release à ce qu'ils promettent. | |
| 58 | + | |
| 59 | +Tout le reste — le buffer, les fenêtres, le client LSP, les terminaux, l'arbre, les snippets, les thèmes — est testé dans turbo-core. | |
| 60 | + | |
| 61 | +## Tester sur un turbo-core non publié | |
| 62 | + | |
| 63 | +L'essentiel de Turbo Golo est turbo-core, et ce dépôt en dépend par version, depuis le proxy de modules : | |
| 64 | + | |
| 65 | +``` | |
| 66 | +require rickub.com/turbo-editors/turbo-core v0.5.0 | |
| 67 | +``` | |
| 68 | + | |
| 69 | +Une modification faite dans une copie de turbo-core placée à côté de celle-ci est donc invisible ici tant qu'elle n'est pas publiée. Pour la tester avant, créez un espace de travail : | |
| 70 | + | |
| 71 | +```bash | |
| 72 | +go work init . ../turbo-core | |
| 73 | +make test | |
| 74 | +``` | |
| 75 | + | |
| 76 | +Chaque import de la bibliothèque pointe désormais sur cette copie. Ni `go.mod` ni `go.sum` ne changent : il n'y a donc rien à défaire. Vérifiez que c'est bien pris en compte — c'est l'erreur contre laquelle il faut se prémunir, car sinon tout compile et tout passe quand même : | |
| 77 | + | |
| 78 | +```bash | |
| 79 | +go list -f '{{.Dir}}' rickub.com/turbo-editors/turbo-core/app | |
| 80 | +``` | |
| 81 | + | |
| 82 | +La réponse doit être votre copie de travail, pas un chemin sous `pkg/mod`. Une fois terminé, `rm go.work go.work.sum` ; le fichier est ignoré par git, il ne peut donc pas être commité par accident. | |
| 83 | + | |
| 84 | +## Qualité du code | |
| 85 | + | |
| 86 | +La suite de tests n'est pas toute la porte de qualité. Celle-ci se mesure à part : | |
| 87 | + | |
| 88 | +```bash | |
| 89 | +python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace . | |
| 90 | +``` | |
| 91 | + | |
| 92 | +Elle écrit un rapport sous `.quality/` et sort en erreur si la porte échoue. | |
| 93 | + | |
| 94 | +## Voir aussi | |
| 95 | + | |
| 96 | +- Pourquoi les tests ont cette forme : [Architecture](../explanation/architecture.md) | |
| 97 | +- Toutes les cibles make : [référence de la ligne de commande](../reference/cli.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,97 @@ | |||
| 1 | +# Lancer les tests | ||
| 2 | + | ||
| 3 | +Ce guide montre comment exécuter et lire la suite de tests de Turbo Golo. Il suppose que vous avez un clone du dépôt et Go 1.26 ou plus récent. | ||
| 4 | + | ||
| 5 | +## Toute la suite | ||
| 6 | + | ||
| 7 | +```bash | ||
| 8 | +make test | ||
| 9 | +``` | ||
| 10 | + | ||
| 11 | +C'est la commande unique documentée. Elle exécute `go test ./...` sur tous les paquets. | ||
| 12 | + | ||
| 13 | +## Variantes | ||
| 14 | + | ||
| 15 | +**Voir chaque test par son nom :** | ||
| 16 | + | ||
| 17 | +```bash | ||
| 18 | +make test-verbose | ||
| 19 | +``` | ||
| 20 | + | ||
| 21 | +**Mesurer la couverture par paquet :** | ||
| 22 | + | ||
| 23 | +```bash | ||
| 24 | +make cover | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +**Un seul paquet :** | ||
| 28 | + | ||
| 29 | +```bash | ||
| 30 | +go test ./internal/gololang/ | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +**Sans lancer de serveur de langage, et sans compiler l'éditeur entier.** Les tests de `internal/gololang` qui portent `WithRealGoloLSP` dans leur nom démarrent un vrai `golo lsp` s'ils en trouvent un, et les tests de l'installeur compilent tout l'éditeur. Pour les sauter : | ||
| 34 | + | ||
| 35 | +```bash | ||
| 36 | +go test -short ./... | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +**Tout ce qu'un commit devrait passer :** | ||
| 40 | + | ||
| 41 | +```bash | ||
| 42 | +make check | ||
| 43 | +``` | ||
| 44 | + | ||
| 45 | +Cela enchaîne `go fmt`, `go vet` et les tests, dans cet ordre. | ||
| 46 | + | ||
| 47 | +## Ce que la suite couvre | ||
| 48 | + | ||
| 49 | +Aucun test n'a besoin d'un vrai terminal : l'éditeur est dessiné sur le `SimulationScreen` de tcell — un vrai `Screen` qui dessine en mémoire — si bien que les assertions portent sur l'image qu'un terminal afficherait réellement. | ||
| 50 | + | ||
| 51 | +Ce que ce dépôt teste est ce qui n'appartient qu'à lui : | ||
| 52 | + | ||
| 53 | +- **Le scanner.** Chaque construction que le scanner colore, et chaque chose qu'il refuse de colorer — `0xFF` n'est pas un nombre, `1_000` non plus — ; une chaîne ou un commentaire `----` portés jusqu'à leur fermeture, sur autant de lignes qu'il faut ; et deux tests qui tiennent la table des mots-clés et celle des fonctions intégrées à ce que le vrai `golo lsp` propose. | ||
| 54 | +- **Le profil.** Le nom de l'éditeur, le menu **Golo** et sa touche `Alt-G` qui n'entre en conflit avec aucun menu fixe, l'absence de marqueur de projet, le serveur qui est l'interpréteur en mode `lsp`, et le dossier `/usr/local/bin` où il est cherché après le `PATH`. | ||
| 55 | +- **Les fichiers de départ.** Chaque snippet se charge et vise `golo`, les corps sont indentés de deux espaces et écrits en chaînes littérales TOML ; chaque outil se charge et lance `golo`, `gogolo` ou `wagolo` ; deux outils d'un même menu ne se disputent jamais une lettre. | ||
| 56 | +- **Le serveur, pour de vrai.** Complétion, déclaration, description d'une fonction avec le commentaire `#` au-dessus d'elle, plan du fichier, références et implémentation d'un appel, recherche d'un symbole dans le projet, diagnostic d'un fichier qui ne s'analyse pas et d'un commentaire à la C — et un test qui vérifie que la définition de type, que `golo lsp` n'annonce pas, répond bien « rien » ; si un futur `golo` l'apprend, ce test le dira, comme il l'a dit quand GoloScript v0.2.0 a appris les références, les implémentations et les symboles du projet. Ces tests **se sautent eux-mêmes** quand `golo` n'est pas installé : un clone sans GoloScript a donc quand même une suite verte. | ||
| 57 | +- **L'installeur et la version.** Un test compile l'éditeur par `scripts/install.sh` dans un préfixe temporaire et vérifie qu'il annonce `Turbo Golo` et le commit ; d'autres tiennent `scripts/check-version.sh` et les scripts de release à ce qu'ils promettent. | ||
| 58 | + | ||
| 59 | +Tout le reste — le buffer, les fenêtres, le client LSP, les terminaux, l'arbre, les snippets, les thèmes — est testé dans turbo-core. | ||
| 60 | + | ||
| 61 | +## Tester sur un turbo-core non publié | ||
| 62 | + | ||
| 63 | +L'essentiel de Turbo Golo est turbo-core, et ce dépôt en dépend par version, depuis le proxy de modules : | ||
| 64 | + | ||
| 65 | +``` | ||
| 66 | +require rickub.com/turbo-editors/turbo-core v0.5.0 | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +Une modification faite dans une copie de turbo-core placée à côté de celle-ci est donc invisible ici tant qu'elle n'est pas publiée. Pour la tester avant, créez un espace de travail : | ||
| 70 | + | ||
| 71 | +```bash | ||
| 72 | +go work init . ../turbo-core | ||
| 73 | +make test | ||
| 74 | +``` | ||
| 75 | + | ||
| 76 | +Chaque import de la bibliothèque pointe désormais sur cette copie. Ni `go.mod` ni `go.sum` ne changent : il n'y a donc rien à défaire. Vérifiez que c'est bien pris en compte — c'est l'erreur contre laquelle il faut se prémunir, car sinon tout compile et tout passe quand même : | ||
| 77 | + | ||
| 78 | +```bash | ||
| 79 | +go list -f '{{.Dir}}' rickub.com/turbo-editors/turbo-core/app | ||
| 80 | +``` | ||
| 81 | + | ||
| 82 | +La réponse doit être votre copie de travail, pas un chemin sous `pkg/mod`. Une fois terminé, `rm go.work go.work.sum` ; le fichier est ignoré par git, il ne peut donc pas être commité par accident. | ||
| 83 | + | ||
| 84 | +## Qualité du code | ||
| 85 | + | ||
| 86 | +La suite de tests n'est pas toute la porte de qualité. Celle-ci se mesure à part : | ||
| 87 | + | ||
| 88 | +```bash | ||
| 89 | +python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace . | ||
| 90 | +``` | ||
| 91 | + | ||
| 92 | +Elle écrit un rapport sous `.quality/` et sort en erreur si la porte échoue. | ||
| 93 | + | ||
| 94 | +## Voir aussi | ||
| 95 | + | ||
| 96 | +- Pourquoi les tests ont cette forme : [Architecture](../explanation/architecture.md) | ||
| 97 | +- Toutes les cibles make : [référence de la ligne de commande](../reference/cli.md) | ||
added
docs/fr/how-to/talk-to-an-agent.md +177 -0 | new file mode 100644 | ||
| @@ -0,0 +1,177 @@ | ||
| 1 | +# Dialoguer avec un agent de code depuis l'éditeur | |
| 2 | + | |
| 3 | +Ce guide montre comment pointer Turbo Golo vers un agent qui parle l'[Agent Client Protocol](https://agentclientprotocol.com), ouvrir une fenêtre dessus, et tenir une conversation sur le code en cours d'édition. Il suppose que Turbo Golo est déjà lancé dans un projet. | |
| 4 | + | |
| 5 | +Turbo Golo est un **client** ACP. Il lance l'agent comme processus fils et lui parle en JSON-RPC sur son entrée et sa sortie standard — le même montage que Zed, donc un agent qui fonctionne là-bas fonctionne ici. | |
| 6 | + | |
| 7 | +## Déclarer un agent à l'éditeur | |
| 8 | + | |
| 9 | +Les agents sont listés dans `acp.toml`. Choisissez **Agent ▸ Create agents file** et l'éditeur écrit un fichier de départ dans `.turbo-gololo/acp.toml`, puis l'ouvre. | |
| 10 | + | |
| 11 | +Un agent, c'est un bloc `[[agent]]` : | |
| 12 | + | |
| 13 | +```toml | |
| 14 | +[[agent]] | |
| 15 | +name = "Bob (llama.cpp)" | |
| 16 | +command = "docker" | |
| 17 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | |
| 18 | +env = { TELEMETRY_ENABLED = "false" } | |
| 19 | +``` | |
| 20 | + | |
| 21 | +`name` est ce qu'affiche le menu Agent et le nom de la fenêtre. `command` et `args` disent comment démarrer l'agent. C'est tout — le fichier est relu à chaque ouverture de fenêtre, donc on ne redémarre jamais l'éditeur pour essayer une modification. | |
| 22 | + | |
| 23 | +Listez-en autant que vous voulez. Chacun devient une ligne du menu, et chaque fenêtre ouverte depuis cette ligne est un processus distinct avec sa propre conversation. | |
| 24 | + | |
| 25 | +## Placer la configuration de l'agent à côté | |
| 26 | + | |
| 27 | +La plupart des agents ont leur propre fichier de configuration, et `.turbo-gololo/` est un endroit raisonnable pour le garder afin qu'il voyage avec le projet. Pour `docker agent`, l'enregistrer sous `.turbo-gololo/agent.yaml` correspond aux `args` ci-dessus : | |
| 28 | + | |
| 29 | +```yaml | |
| 30 | +providers: | |
| 31 | + llamacpp: | |
| 32 | + api_type: openai_chatcompletions | |
| 33 | + base_url: http://localhost:8080/v1 | |
| 34 | + | |
| 35 | +models: | |
| 36 | + mellum2: | |
| 37 | + provider: llamacpp | |
| 38 | + model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M | |
| 39 | + temperature: 0.7 | |
| 40 | + provider_opts: | |
| 41 | + context_size: 262144 | |
| 42 | + | |
| 43 | +agents: | |
| 44 | + root: | |
| 45 | + model: mellum2 | |
| 46 | + description: A helpful AI assistant running on a local llama.cpp server | |
| 47 | + instruction: | | |
| 48 | + You name is Bob 🤓, you are a knowledgeable code assistant. | |
| 49 | + Be helpful, accurate, and concise in your responses. | |
| 50 | + You have access to the local filesystem and shell: use these tools | |
| 51 | + toolsets: | |
| 52 | + - type: filesystem | |
| 53 | + - type: shell | |
| 54 | +``` | |
| 55 | + | |
| 56 | +## Ouvrir une fenêtre dessus | |
| 57 | + | |
| 58 | +Appuyez sur `Alt-A`, ou choisissez **Agent** dans la barre de menus, puis l'agent par son nom. | |
| 59 | + | |
| 60 | +Une fenêtre s'ouvre, coupée en deux : la conversation en haut, une zone de saisie en bas. L'agent est démarré à l'ouverture de la fenêtre et arrêté à sa fermeture. | |
| 61 | + | |
| 62 | +``` | |
| 63 | +┌ Bob (llama.cpp) ───────────────────────────────[■]┐ | |
| 64 | +│ ‣ Vous │ | |
| 65 | +│ Que fait buildMenus ? │ | |
| 66 | +│ │ | |
| 67 | +│ ‣ Shell ls -1 internal/ ✓ fini │ | |
| 68 | +│ golang │ | |
| 69 | +│ │ | |
| 70 | +│ ‣ Bob │ | |
| 71 | +│ Elle assemble la barre de menus. Sa forme : │ | |
| 72 | +│ │ | |
| 73 | +│ ```golo │ | |
| 74 | +│ func (a *App) buildMenus() *ui.MenuBar { │ | |
| 75 | +│ return ui.NewMenuBar(a.allMenus()...) │ | |
| 76 | +│ } │ | |
| 77 | +│ ``` │ | |
| 78 | +├───────────────────────────────────────────────────┤ | |
| 79 | +│ > _ │ | |
| 80 | +└───────────────────────────────────────────────────┘ | |
| 81 | +``` | |
| 82 | + | |
| 83 | +Le code que l'agent envoie dans un bloc délimité est coloré par les mêmes analyseurs que l'éditeur utilise pour les fichiers : une réponse en Golo est colorée comme du Golo, une réponse en shell comme du shell. Un bloc annonçant un langage que l'éditeur ne colore pas est laissé brut plutôt que deviné. | |
| 84 | + | |
| 85 | +## Tenir la conversation | |
| 86 | + | |
| 87 | +| Touche | Effet | | |
| 88 | +| --- | --- | | |
| 89 | +| `Entrée` | Envoyer ce qui est saisi | | |
| 90 | +| `Alt-Entrée` | Passer à la ligne au lieu d'envoyer | | |
| 91 | +| `Tab` | Passer de la conversation à la zone de saisie, et retour | | |
| 92 | +| `PgUp` `PgDn` | Faire défiler la conversation d'un écran | | |
| 93 | +| `Échap` | Arrêter le tour en cours | | |
| 94 | +| `Ctrl-W` | Fermer la fenêtre, et arrêter l'agent avec elle | | |
| 95 | + | |
| 96 | +Pendant que l'agent répond, sa réponse s'affiche au fil de l'écriture plutôt que d'un bloc, et la règle entre les deux zones fait tourner un indicateur à côté du mot *thinking*. `Échap` l'interrompt — l'agent reçoit l'ordre de s'arrêter, et ce qu'il avait déjà dit reste dans la fenêtre. | |
| 97 | + | |
| 98 | +## Utiliser les commandes propres à l'agent | |
| 99 | + | |
| 100 | +Certains agents répondent à des commandes — `/compact`, `/web`, `/plan` — et disent à l'éditeur lesquelles. Tapez `/` comme premier caractère de la zone de saisie et la liste s'ouvre par-dessus la conversation : chaque commande, ce qu'elle fait, et entre chevrons ce qu'elle attend après son nom. | |
| 101 | + | |
| 102 | +Continuez à taper pour la réduire, `↑` `↓` pour vous déplacer, puis `Tab` pour compléter. Une commande qui attend quelque chose est complétée avec une espace après elle, prête à recevoir la suite ; appuyez sur `Entrée` quand la ligne dit ce que vous voulez. Si rien n'apparaît quand vous tapez `/`, l'agent n'a annoncé aucune commande — **Agent ▸ Agent status** le dit — et `/` n'est qu'un caractère. | |
| 103 | + | |
| 104 | +## Désigner un fichier à l'agent | |
| 105 | + | |
| 106 | +Tapez `@` n'importe où dans la zone de saisie et les fichiers du projet apparaissent. Tapez quelques lettres du nom du fichier pour réduire la liste, `Tab` pour prendre celui en surbrillance : | |
| 107 | + | |
| 108 | +``` | |
| 109 | +> explique ce que fait @internal/scanner.go | |
| 110 | +``` | |
| 111 | + | |
| 112 | +Quand vous appuyez sur `Entrée`, l'agent reçoit le **fichier**, et pas seulement son nom : son texte quand l'agent accepte le contexte incorporé, un lien vers lui sinon. Si le fichier est ouvert dans l'éditeur avec des modifications non enregistrées, c'est votre version non enregistrée qui part. La ligne reste dans la conversation telle que vous l'avez tapée. | |
| 113 | + | |
| 114 | +Plusieurs fichiers dans une invite, c'est plusieurs `@`. Un mot qui commence par `@` mais n'est pas un fichier — une adresse électronique — est laissé en texte. | |
| 115 | + | |
| 116 | +## Récupérer un morceau de la conversation | |
| 117 | + | |
| 118 | +Appuyez sur `Tab` pour placer le curseur dans la conversation. La règle change et annonce ce que font désormais les touches. | |
| 119 | + | |
| 120 | +| Touche | Effet | | |
| 121 | +| --- | --- | | |
| 122 | +| `↑` `↓` `PgUp` `PgDn` | Déplacer le curseur dans ce qui a été dit | | |
| 123 | +| `Shift-↑` `Shift-↓` | Sélectionner des lignes entières | | |
| 124 | +| Glisser à la souris | Pareil, à la main | | |
| 125 | +| `Ctrl-C` | Copier | | |
| 126 | +| `Échap` | Abandonner la sélection | | |
| 127 | +| `Tab` | Revenir à la zone de saisie | | |
| 128 | + | |
| 129 | +**Sans rien de sélectionné, `Ctrl-C` copie le bloc sur lequel est le curseur** — un bloc de code délimité, un paragraphe, la sortie d'un outil — sans le libellé de l'interlocuteur au-dessus ni la phrase qui suit. C'est presque toujours ce que vous vouliez, et cela évite de le sélectionner à la main. | |
| 130 | + | |
| 131 | +Ce qui est copié va dans **deux** presse-papiers : celui de l'éditeur, pour que `Shift-Ins` le colle dans un fichier ouvert ici, et celui du système, pour que `Ctrl-V` le colle n'importe où ailleurs. L'indentation d'affichage de la conversation est retirée, donc le code collé arrive collé à la marge. | |
| 132 | + | |
| 133 | +La moitié « système » passe par votre terminal (une séquence d'échappement nommée OSC 52). La plupart des terminaux la gèrent ; quelques-uns la refusent par sécurité, et certains demandent de l'activer. Si `Ctrl-V` ailleurs ne donne rien, c'est là qu'il faut regarder — le presse-papiers de l'éditeur contient le texte dans tous les cas. | |
| 134 | + | |
| 135 | +## Répondre quand l'agent demande la permission | |
| 136 | + | |
| 137 | +Un agent doté d'un outil shell ou d'un outil de fichiers demande avant de s'en servir. Une boîte de dialogue nomme l'outil et la commande exacte, et propose les choix que l'agent lui-même a proposés — d'ordinaire *Autoriser*, *Autoriser et retenir mon choix*, et *Passer*. | |
| 138 | + | |
| 139 | +``` | |
| 140 | +┌────────── Bob (llama.cpp) veut lancer ──────────┐ | |
| 141 | +│ │ | |
| 142 | +│ Shell │ | |
| 143 | +│ ls -1 │ | |
| 144 | +│ │ | |
| 145 | +│ [ Autoriser ] [ Toujours ] [ Passer ] │ | |
| 146 | +└─────────────────────────────────────────────────┘ | |
| 147 | +``` | |
| 148 | + | |
| 149 | +*Toujours* est retenu par l'agent, pas par l'éditeur : ce que cela couvre et combien de temps cela dure sont l'affaire de l'agent. Échap équivaut à *Passer*. | |
| 150 | + | |
| 151 | +Rien ne s'exécute avant votre réponse. Un agent en attente d'une boîte de permission est simplement bloqué, et c'est bien le but. | |
| 152 | + | |
| 153 | +## Laisser l'agent voir ce qui n'est pas encore enregistré | |
| 154 | + | |
| 155 | +L'éditeur offre à l'agent son propre système de fichiers : quand l'agent lit un fichier que vous avez ouvert avec des modifications non enregistrées, il reçoit **le texte du tampon**, pas le texte plus ancien du disque. C'est généralement ce qu'on veut — vous posez une question sur la modification que vous venez de faire. | |
| 156 | + | |
| 157 | +Quand l'agent écrit un fichier, la modification arrive dans le tampon et la fenêtre est marquée modifiée : vous pouvez la lire, l'annuler avec `Ctrl-Z`, ou l'enregistrer avec `F2`. Un fichier que vous n'avez pas ouvert est lu et écrit directement sur le disque. | |
| 158 | + | |
| 159 | +## Faire tourner plusieurs agents à la fois | |
| 160 | + | |
| 161 | +Chaque fenêtre est son propre processus et sa propre conversation. Ouvrir deux fois le même agent donne deux sessions indépendantes, et ouvrir deux agents différents permet de mettre côte à côte un modèle local rapide et un modèle lent et soigneux — **Window ▸ Tile** les dispose. | |
| 162 | + | |
| 163 | +Quitter l'éditeur arrête tous les agents. | |
| 164 | + | |
| 165 | +## Variantes | |
| 166 | + | |
| 167 | +- **Vous voulez que l'agent tourne ailleurs qu'à la racine du projet.** Ajoutez `cwd = "backend"` à son bloc. Le chemin est relatif au projet, et c'est à la fois l'endroit où le processus démarre et le dossier de travail annoncé à l'agent. | |
| 168 | +- **L'agent a besoin d'un identifiant.** Mettez-le dans `env`, ou comptez sur sa présence dans l'environnement depuis lequel vous lancez l'éditeur — l'agent en hérite. | |
| 169 | +- **Les commandes de l'agent n'apparaissent pas quand vous tapez `/`.** Ouvrez **Agent ▸ Agent status** avec la fenêtre devant. S'il ne liste aucune commande, l'agent n'en a annoncé aucune — ou les a annoncées dans une forme que cet éditeur n'a pas su lire, auquel cas la boîte nomme la mise à jour et l'erreur de décodage. Pour voir exactement ce qui est passé sur le fil, lancez l'éditeur avec `TURBO_ACP_TRACE=/tmp/acp.log` et lisez le fichier : `->` est ce que l'éditeur a envoyé, `<-` ce que l'agent a répondu. | |
| 170 | +- **L'agent ne démarre pas.** **Agent ▸ Agent status** liste ce qui a été lu dans `acp.toml`, la ligne de commande obtenue pour chaque agent, et l'erreur de tout ce qui n'a pas démarré. Ce que l'agent écrit sur sa sortie d'erreur y figure aussi, et c'est là qu'un point d'accès de modèle mal configuré se signale. | |
| 171 | +- **Vous gardez le même agent dans tous les projets.** Mettez le bloc `[[agent]]` dans `~/.config/turbo-golo/acp.toml`. Le fichier du projet est lu ensuite, et un agent du même `name` y remplace le vôtre. | |
| 172 | + | |
| 173 | +## Voir aussi | |
| 174 | + | |
| 175 | +- Chaque clé du fichier, et la part exacte du protocole implémentée : [référence Agents et ACP](../reference/acp.md) | |
| 176 | +- Pourquoi un agent est une fenêtre et non un panneau, et pourquoi les permissions sont modales : [Fenêtres agent](../explanation/agent-windows.md) | |
| 177 | +- Le protocole lui-même : [agentclientprotocol.com](https://agentclientprotocol.com) | |
| new file mode 100644 | |||
| @@ -0,0 +1,177 @@ | |||
| 1 | +# Dialoguer avec un agent de code depuis l'éditeur | ||
| 2 | + | ||
| 3 | +Ce guide montre comment pointer Turbo Golo vers un agent qui parle l'[Agent Client Protocol](https://agentclientprotocol.com), ouvrir une fenêtre dessus, et tenir une conversation sur le code en cours d'édition. Il suppose que Turbo Golo est déjà lancé dans un projet. | ||
| 4 | + | ||
| 5 | +Turbo Golo est un **client** ACP. Il lance l'agent comme processus fils et lui parle en JSON-RPC sur son entrée et sa sortie standard — le même montage que Zed, donc un agent qui fonctionne là-bas fonctionne ici. | ||
| 6 | + | ||
| 7 | +## Déclarer un agent à l'éditeur | ||
| 8 | + | ||
| 9 | +Les agents sont listés dans `acp.toml`. Choisissez **Agent ▸ Create agents file** et l'éditeur écrit un fichier de départ dans `.turbo-gololo/acp.toml`, puis l'ouvre. | ||
| 10 | + | ||
| 11 | +Un agent, c'est un bloc `[[agent]]` : | ||
| 12 | + | ||
| 13 | +```toml | ||
| 14 | +[[agent]] | ||
| 15 | +name = "Bob (llama.cpp)" | ||
| 16 | +command = "docker" | ||
| 17 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | ||
| 18 | +env = { TELEMETRY_ENABLED = "false" } | ||
| 19 | +``` | ||
| 20 | + | ||
| 21 | +`name` est ce qu'affiche le menu Agent et le nom de la fenêtre. `command` et `args` disent comment démarrer l'agent. C'est tout — le fichier est relu à chaque ouverture de fenêtre, donc on ne redémarre jamais l'éditeur pour essayer une modification. | ||
| 22 | + | ||
| 23 | +Listez-en autant que vous voulez. Chacun devient une ligne du menu, et chaque fenêtre ouverte depuis cette ligne est un processus distinct avec sa propre conversation. | ||
| 24 | + | ||
| 25 | +## Placer la configuration de l'agent à côté | ||
| 26 | + | ||
| 27 | +La plupart des agents ont leur propre fichier de configuration, et `.turbo-gololo/` est un endroit raisonnable pour le garder afin qu'il voyage avec le projet. Pour `docker agent`, l'enregistrer sous `.turbo-gololo/agent.yaml` correspond aux `args` ci-dessus : | ||
| 28 | + | ||
| 29 | +```yaml | ||
| 30 | +providers: | ||
| 31 | + llamacpp: | ||
| 32 | + api_type: openai_chatcompletions | ||
| 33 | + base_url: http://localhost:8080/v1 | ||
| 34 | + | ||
| 35 | +models: | ||
| 36 | + mellum2: | ||
| 37 | + provider: llamacpp | ||
| 38 | + model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M | ||
| 39 | + temperature: 0.7 | ||
| 40 | + provider_opts: | ||
| 41 | + context_size: 262144 | ||
| 42 | + | ||
| 43 | +agents: | ||
| 44 | + root: | ||
| 45 | + model: mellum2 | ||
| 46 | + description: A helpful AI assistant running on a local llama.cpp server | ||
| 47 | + instruction: | | ||
| 48 | + You name is Bob 🤓, you are a knowledgeable code assistant. | ||
| 49 | + Be helpful, accurate, and concise in your responses. | ||
| 50 | + You have access to the local filesystem and shell: use these tools | ||
| 51 | + toolsets: | ||
| 52 | + - type: filesystem | ||
| 53 | + - type: shell | ||
| 54 | +``` | ||
| 55 | + | ||
| 56 | +## Ouvrir une fenêtre dessus | ||
| 57 | + | ||
| 58 | +Appuyez sur `Alt-A`, ou choisissez **Agent** dans la barre de menus, puis l'agent par son nom. | ||
| 59 | + | ||
| 60 | +Une fenêtre s'ouvre, coupée en deux : la conversation en haut, une zone de saisie en bas. L'agent est démarré à l'ouverture de la fenêtre et arrêté à sa fermeture. | ||
| 61 | + | ||
| 62 | +``` | ||
| 63 | +┌ Bob (llama.cpp) ───────────────────────────────[■]┐ | ||
| 64 | +│ ‣ Vous │ | ||
| 65 | +│ Que fait buildMenus ? │ | ||
| 66 | +│ │ | ||
| 67 | +│ ‣ Shell ls -1 internal/ ✓ fini │ | ||
| 68 | +│ golang │ | ||
| 69 | +│ │ | ||
| 70 | +│ ‣ Bob │ | ||
| 71 | +│ Elle assemble la barre de menus. Sa forme : │ | ||
| 72 | +│ │ | ||
| 73 | +│ ```golo │ | ||
| 74 | +│ func (a *App) buildMenus() *ui.MenuBar { │ | ||
| 75 | +│ return ui.NewMenuBar(a.allMenus()...) │ | ||
| 76 | +│ } │ | ||
| 77 | +│ ``` │ | ||
| 78 | +├───────────────────────────────────────────────────┤ | ||
| 79 | +│ > _ │ | ||
| 80 | +└───────────────────────────────────────────────────┘ | ||
| 81 | +``` | ||
| 82 | + | ||
| 83 | +Le code que l'agent envoie dans un bloc délimité est coloré par les mêmes analyseurs que l'éditeur utilise pour les fichiers : une réponse en Golo est colorée comme du Golo, une réponse en shell comme du shell. Un bloc annonçant un langage que l'éditeur ne colore pas est laissé brut plutôt que deviné. | ||
| 84 | + | ||
| 85 | +## Tenir la conversation | ||
| 86 | + | ||
| 87 | +| Touche | Effet | | ||
| 88 | +| --- | --- | | ||
| 89 | +| `Entrée` | Envoyer ce qui est saisi | | ||
| 90 | +| `Alt-Entrée` | Passer à la ligne au lieu d'envoyer | | ||
| 91 | +| `Tab` | Passer de la conversation à la zone de saisie, et retour | | ||
| 92 | +| `PgUp` `PgDn` | Faire défiler la conversation d'un écran | | ||
| 93 | +| `Échap` | Arrêter le tour en cours | | ||
| 94 | +| `Ctrl-W` | Fermer la fenêtre, et arrêter l'agent avec elle | | ||
| 95 | + | ||
| 96 | +Pendant que l'agent répond, sa réponse s'affiche au fil de l'écriture plutôt que d'un bloc, et la règle entre les deux zones fait tourner un indicateur à côté du mot *thinking*. `Échap` l'interrompt — l'agent reçoit l'ordre de s'arrêter, et ce qu'il avait déjà dit reste dans la fenêtre. | ||
| 97 | + | ||
| 98 | +## Utiliser les commandes propres à l'agent | ||
| 99 | + | ||
| 100 | +Certains agents répondent à des commandes — `/compact`, `/web`, `/plan` — et disent à l'éditeur lesquelles. Tapez `/` comme premier caractère de la zone de saisie et la liste s'ouvre par-dessus la conversation : chaque commande, ce qu'elle fait, et entre chevrons ce qu'elle attend après son nom. | ||
| 101 | + | ||
| 102 | +Continuez à taper pour la réduire, `↑` `↓` pour vous déplacer, puis `Tab` pour compléter. Une commande qui attend quelque chose est complétée avec une espace après elle, prête à recevoir la suite ; appuyez sur `Entrée` quand la ligne dit ce que vous voulez. Si rien n'apparaît quand vous tapez `/`, l'agent n'a annoncé aucune commande — **Agent ▸ Agent status** le dit — et `/` n'est qu'un caractère. | ||
| 103 | + | ||
| 104 | +## Désigner un fichier à l'agent | ||
| 105 | + | ||
| 106 | +Tapez `@` n'importe où dans la zone de saisie et les fichiers du projet apparaissent. Tapez quelques lettres du nom du fichier pour réduire la liste, `Tab` pour prendre celui en surbrillance : | ||
| 107 | + | ||
| 108 | +``` | ||
| 109 | +> explique ce que fait @internal/scanner.go | ||
| 110 | +``` | ||
| 111 | + | ||
| 112 | +Quand vous appuyez sur `Entrée`, l'agent reçoit le **fichier**, et pas seulement son nom : son texte quand l'agent accepte le contexte incorporé, un lien vers lui sinon. Si le fichier est ouvert dans l'éditeur avec des modifications non enregistrées, c'est votre version non enregistrée qui part. La ligne reste dans la conversation telle que vous l'avez tapée. | ||
| 113 | + | ||
| 114 | +Plusieurs fichiers dans une invite, c'est plusieurs `@`. Un mot qui commence par `@` mais n'est pas un fichier — une adresse électronique — est laissé en texte. | ||
| 115 | + | ||
| 116 | +## Récupérer un morceau de la conversation | ||
| 117 | + | ||
| 118 | +Appuyez sur `Tab` pour placer le curseur dans la conversation. La règle change et annonce ce que font désormais les touches. | ||
| 119 | + | ||
| 120 | +| Touche | Effet | | ||
| 121 | +| --- | --- | | ||
| 122 | +| `↑` `↓` `PgUp` `PgDn` | Déplacer le curseur dans ce qui a été dit | | ||
| 123 | +| `Shift-↑` `Shift-↓` | Sélectionner des lignes entières | | ||
| 124 | +| Glisser à la souris | Pareil, à la main | | ||
| 125 | +| `Ctrl-C` | Copier | | ||
| 126 | +| `Échap` | Abandonner la sélection | | ||
| 127 | +| `Tab` | Revenir à la zone de saisie | | ||
| 128 | + | ||
| 129 | +**Sans rien de sélectionné, `Ctrl-C` copie le bloc sur lequel est le curseur** — un bloc de code délimité, un paragraphe, la sortie d'un outil — sans le libellé de l'interlocuteur au-dessus ni la phrase qui suit. C'est presque toujours ce que vous vouliez, et cela évite de le sélectionner à la main. | ||
| 130 | + | ||
| 131 | +Ce qui est copié va dans **deux** presse-papiers : celui de l'éditeur, pour que `Shift-Ins` le colle dans un fichier ouvert ici, et celui du système, pour que `Ctrl-V` le colle n'importe où ailleurs. L'indentation d'affichage de la conversation est retirée, donc le code collé arrive collé à la marge. | ||
| 132 | + | ||
| 133 | +La moitié « système » passe par votre terminal (une séquence d'échappement nommée OSC 52). La plupart des terminaux la gèrent ; quelques-uns la refusent par sécurité, et certains demandent de l'activer. Si `Ctrl-V` ailleurs ne donne rien, c'est là qu'il faut regarder — le presse-papiers de l'éditeur contient le texte dans tous les cas. | ||
| 134 | + | ||
| 135 | +## Répondre quand l'agent demande la permission | ||
| 136 | + | ||
| 137 | +Un agent doté d'un outil shell ou d'un outil de fichiers demande avant de s'en servir. Une boîte de dialogue nomme l'outil et la commande exacte, et propose les choix que l'agent lui-même a proposés — d'ordinaire *Autoriser*, *Autoriser et retenir mon choix*, et *Passer*. | ||
| 138 | + | ||
| 139 | +``` | ||
| 140 | +┌────────── Bob (llama.cpp) veut lancer ──────────┐ | ||
| 141 | +│ │ | ||
| 142 | +│ Shell │ | ||
| 143 | +│ ls -1 │ | ||
| 144 | +│ │ | ||
| 145 | +│ [ Autoriser ] [ Toujours ] [ Passer ] │ | ||
| 146 | +└─────────────────────────────────────────────────┘ | ||
| 147 | +``` | ||
| 148 | + | ||
| 149 | +*Toujours* est retenu par l'agent, pas par l'éditeur : ce que cela couvre et combien de temps cela dure sont l'affaire de l'agent. Échap équivaut à *Passer*. | ||
| 150 | + | ||
| 151 | +Rien ne s'exécute avant votre réponse. Un agent en attente d'une boîte de permission est simplement bloqué, et c'est bien le but. | ||
| 152 | + | ||
| 153 | +## Laisser l'agent voir ce qui n'est pas encore enregistré | ||
| 154 | + | ||
| 155 | +L'éditeur offre à l'agent son propre système de fichiers : quand l'agent lit un fichier que vous avez ouvert avec des modifications non enregistrées, il reçoit **le texte du tampon**, pas le texte plus ancien du disque. C'est généralement ce qu'on veut — vous posez une question sur la modification que vous venez de faire. | ||
| 156 | + | ||
| 157 | +Quand l'agent écrit un fichier, la modification arrive dans le tampon et la fenêtre est marquée modifiée : vous pouvez la lire, l'annuler avec `Ctrl-Z`, ou l'enregistrer avec `F2`. Un fichier que vous n'avez pas ouvert est lu et écrit directement sur le disque. | ||
| 158 | + | ||
| 159 | +## Faire tourner plusieurs agents à la fois | ||
| 160 | + | ||
| 161 | +Chaque fenêtre est son propre processus et sa propre conversation. Ouvrir deux fois le même agent donne deux sessions indépendantes, et ouvrir deux agents différents permet de mettre côte à côte un modèle local rapide et un modèle lent et soigneux — **Window ▸ Tile** les dispose. | ||
| 162 | + | ||
| 163 | +Quitter l'éditeur arrête tous les agents. | ||
| 164 | + | ||
| 165 | +## Variantes | ||
| 166 | + | ||
| 167 | +- **Vous voulez que l'agent tourne ailleurs qu'à la racine du projet.** Ajoutez `cwd = "backend"` à son bloc. Le chemin est relatif au projet, et c'est à la fois l'endroit où le processus démarre et le dossier de travail annoncé à l'agent. | ||
| 168 | +- **L'agent a besoin d'un identifiant.** Mettez-le dans `env`, ou comptez sur sa présence dans l'environnement depuis lequel vous lancez l'éditeur — l'agent en hérite. | ||
| 169 | +- **Les commandes de l'agent n'apparaissent pas quand vous tapez `/`.** Ouvrez **Agent ▸ Agent status** avec la fenêtre devant. S'il ne liste aucune commande, l'agent n'en a annoncé aucune — ou les a annoncées dans une forme que cet éditeur n'a pas su lire, auquel cas la boîte nomme la mise à jour et l'erreur de décodage. Pour voir exactement ce qui est passé sur le fil, lancez l'éditeur avec `TURBO_ACP_TRACE=/tmp/acp.log` et lisez le fichier : `->` est ce que l'éditeur a envoyé, `<-` ce que l'agent a répondu. | ||
| 170 | +- **L'agent ne démarre pas.** **Agent ▸ Agent status** liste ce qui a été lu dans `acp.toml`, la ligne de commande obtenue pour chaque agent, et l'erreur de tout ce qui n'a pas démarré. Ce que l'agent écrit sur sa sortie d'erreur y figure aussi, et c'est là qu'un point d'accès de modèle mal configuré se signale. | ||
| 171 | +- **Vous gardez le même agent dans tous les projets.** Mettez le bloc `[[agent]]` dans `~/.config/turbo-golo/acp.toml`. Le fichier du projet est lu ensuite, et un agent du même `name` y remplace le vôtre. | ||
| 172 | + | ||
| 173 | +## Voir aussi | ||
| 174 | + | ||
| 175 | +- Chaque clé du fichier, et la part exacte du protocole implémentée : [référence Agents et ACP](../reference/acp.md) | ||
| 176 | +- Pourquoi un agent est une fenêtre et non un panneau, et pourquoi les permissions sont modales : [Fenêtres agent](../explanation/agent-windows.md) | ||
| 177 | +- Le protocole lui-même : [agentclientprotocol.com](https://agentclientprotocol.com) | ||
added
docs/fr/how-to/use-a-terminal.md +62 -0 | new file mode 100644 | ||
| @@ -0,0 +1,62 @@ | ||
| 1 | +# Lancer des commandes shell sans quitter l'éditeur | |
| 2 | + | |
| 3 | +Ce guide montre comment ouvrir une fenêtre terminal, y exécuter et tester le script en cours d'édition, puis revenir au fichier. Il suppose que Turbo Golo est déjà lancé avec un fichier ouvert. | |
| 4 | + | |
| 5 | +## Ouvrir un terminal | |
| 6 | + | |
| 7 | +Appuyez sur `F8`, ou choisissez **Window ▸ New terminal**. | |
| 8 | + | |
| 9 | +Une nouvelle fenêtre s'ouvre avec votre shell, dans le dossier du fichier que vous étiez en train d'éditer. C'est normalement le dossier voulu : `golo --test` et `git diff` portent tous deux sur ce que vous avez sous les yeux. | |
| 10 | + | |
| 11 | +La fenêtre porte le nom du shell, et se renomme dès qu'un programme lancé dedans définit un titre — `vim`, `htop` et `ssh` le font tous. | |
| 12 | + | |
| 13 | +## Lancer quelque chose | |
| 14 | + | |
| 15 | +Tapez dedans comme dans n'importe quel terminal. Le shell reçoit presque toutes les touches, y compris celles que l'éditeur utiliserait autrement : `Ctrl-C` interrompt, `Ctrl-W` supprime un mot, `Ctrl-R` cherche dans l'historique. | |
| 16 | + | |
| 17 | +Ce que l'éditeur conserve est court, et voulu — c'est le chemin de sortie : | |
| 18 | + | |
| 19 | +| Touche | Effet, même avec un terminal au premier plan | | |
| 20 | +| --- | --- | | |
| 21 | +| `F8` | Ouvrir un autre terminal | | |
| 22 | +| `F6` | Passer à la fenêtre suivante | | |
| 23 | +| `F10` | Ouvrir la barre de menus | | |
| 24 | +| `F2` `F3` `F4` | Enregistrer, Ouvrir, Nouveau | | |
| 25 | +| `Alt-1` … `Alt-9` | Passer cette fenêtre au premier plan | | |
| 26 | +| `Alt-X` | Quitter l'éditeur | | |
| 27 | + | |
| 28 | +## Relire ce qui a défilé | |
| 29 | + | |
| 30 | +`Shift-PgUp` et `Shift-PgDn` parcourent l'historique un écran à la fois ; la molette déplace de trois lignes. Deux mille lignes sont conservées. | |
| 31 | + | |
| 32 | +Taper quoi que ce soit ramène directement à l'écran vivant : jamais besoin de redescendre avant de lancer la commande suivante. | |
| 33 | + | |
| 34 | +## Travailler avec le fichier et le shell côte à côte | |
| 35 | + | |
| 36 | +Un terminal est une fenêtre ordinaire, donc toutes les commandes de fenêtre s'y appliquent : | |
| 37 | + | |
| 38 | +- **Window ▸ Tile** place le fichier et le terminal côte à côte. | |
| 39 | +- **Window ▸ Maximise**, ou la case `[■]` à droite de sa barre de titre, donne tout le bureau au terminal pendant qu'un script tourne. La case affiche alors `[▬]`, et l'actionner remet la fenêtre en place. | |
| 40 | +- Tirez son coin inférieur droit pour le redimensionner — le shell est prévenu de sa nouvelle taille, donc `less` et `vim` se réajustent. | |
| 41 | + | |
| 42 | +## Le fermer | |
| 43 | + | |
| 44 | +`Ctrl-W` appartient au shell, pas à l'éditeur : fermer un terminal se fait donc autrement. | |
| 45 | + | |
| 46 | +- **File ▸ Close**, ou | |
| 47 | +- cliquez sur la case `[x]` dans son coin supérieur gauche. | |
| 48 | + | |
| 49 | +L'un comme l'autre terminent le shell qui y tourne. Rien n'est demandé au préalable : un terminal contient un processus en cours, pas un travail non enregistré, et fermer la fenêtre est la façon de dire que vous en avez fini. Quitter l'éditeur ferme tous les terminaux d'un coup. | |
| 50 | + | |
| 51 | +## Variantes | |
| 52 | + | |
| 53 | +- **Vous voulez lancer `golo` sans taper la commande.** Le menu **Golo** (`Alt-G`) la lance pour vous, dans une fenêtre terminal quand elle lit le clavier — le REPL, le débogueur — et dans une popup sinon. Voir [Lancer les commandes golo depuis l'éditeur](run-golo-commands.md). | |
| 54 | +- **Vous voulez un autre shell.** Le shell est pris dans `$SHELL`, avec `/bin/sh` par défaut ; sous Windows dans `%COMSPEC%`, avec `cmd.exe` par défaut. Lancez l'éditeur avec `SHELL=/bin/zsh turbo-golo` pour le changer le temps d'une session. | |
| 55 | +- **Aucun fichier n'est ouvert.** Le terminal démarre dans le dossier depuis lequel l'éditeur a été lancé. | |
| 56 | +- **Vous êtes sous Windows.** Les fenêtres terminal tournent dans une pseudo-console (ConPTY), ce qui demande Windows 10 version 1809 ou plus récent, et le shell est `%COMSPEC%` — cmd.exe. Ce chemin a été compilé et vérifié mais pas encore exécuté par les auteurs, qui travaillent sous Linux et macOS. La première fois, essayez les cinq choses qu'il doit réussir — `F8`, tapez `dir`, redimensionnez la fenêtre, lancez une commande du menu qui dit `output = "terminal"`, et interrompez une commande longue avec `Ctrl-C` — et signalez ce qui ne s'est pas comporté comme attendu. | |
| 57 | + | |
| 58 | +## Voir aussi | |
| 59 | + | |
| 60 | +- Tout ce que le terminal implémente, exactement : [référence des fenêtres terminal](../reference/terminal.md) | |
| 61 | +- Pourquoi il lance un vrai shell plutôt que de capturer la sortie d'une commande : [Fenêtres terminal](../explanation/terminal-windows.md) | |
| 62 | +- Les couleurs qu'il utilise : [Format des fichiers de thème](../reference/themes.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | +# Lancer des commandes shell sans quitter l'éditeur | ||
| 2 | + | ||
| 3 | +Ce guide montre comment ouvrir une fenêtre terminal, y exécuter et tester le script en cours d'édition, puis revenir au fichier. Il suppose que Turbo Golo est déjà lancé avec un fichier ouvert. | ||
| 4 | + | ||
| 5 | +## Ouvrir un terminal | ||
| 6 | + | ||
| 7 | +Appuyez sur `F8`, ou choisissez **Window ▸ New terminal**. | ||
| 8 | + | ||
| 9 | +Une nouvelle fenêtre s'ouvre avec votre shell, dans le dossier du fichier que vous étiez en train d'éditer. C'est normalement le dossier voulu : `golo --test` et `git diff` portent tous deux sur ce que vous avez sous les yeux. | ||
| 10 | + | ||
| 11 | +La fenêtre porte le nom du shell, et se renomme dès qu'un programme lancé dedans définit un titre — `vim`, `htop` et `ssh` le font tous. | ||
| 12 | + | ||
| 13 | +## Lancer quelque chose | ||
| 14 | + | ||
| 15 | +Tapez dedans comme dans n'importe quel terminal. Le shell reçoit presque toutes les touches, y compris celles que l'éditeur utiliserait autrement : `Ctrl-C` interrompt, `Ctrl-W` supprime un mot, `Ctrl-R` cherche dans l'historique. | ||
| 16 | + | ||
| 17 | +Ce que l'éditeur conserve est court, et voulu — c'est le chemin de sortie : | ||
| 18 | + | ||
| 19 | +| Touche | Effet, même avec un terminal au premier plan | | ||
| 20 | +| --- | --- | | ||
| 21 | +| `F8` | Ouvrir un autre terminal | | ||
| 22 | +| `F6` | Passer à la fenêtre suivante | | ||
| 23 | +| `F10` | Ouvrir la barre de menus | | ||
| 24 | +| `F2` `F3` `F4` | Enregistrer, Ouvrir, Nouveau | | ||
| 25 | +| `Alt-1` … `Alt-9` | Passer cette fenêtre au premier plan | | ||
| 26 | +| `Alt-X` | Quitter l'éditeur | | ||
| 27 | + | ||
| 28 | +## Relire ce qui a défilé | ||
| 29 | + | ||
| 30 | +`Shift-PgUp` et `Shift-PgDn` parcourent l'historique un écran à la fois ; la molette déplace de trois lignes. Deux mille lignes sont conservées. | ||
| 31 | + | ||
| 32 | +Taper quoi que ce soit ramène directement à l'écran vivant : jamais besoin de redescendre avant de lancer la commande suivante. | ||
| 33 | + | ||
| 34 | +## Travailler avec le fichier et le shell côte à côte | ||
| 35 | + | ||
| 36 | +Un terminal est une fenêtre ordinaire, donc toutes les commandes de fenêtre s'y appliquent : | ||
| 37 | + | ||
| 38 | +- **Window ▸ Tile** place le fichier et le terminal côte à côte. | ||
| 39 | +- **Window ▸ Maximise**, ou la case `[■]` à droite de sa barre de titre, donne tout le bureau au terminal pendant qu'un script tourne. La case affiche alors `[▬]`, et l'actionner remet la fenêtre en place. | ||
| 40 | +- Tirez son coin inférieur droit pour le redimensionner — le shell est prévenu de sa nouvelle taille, donc `less` et `vim` se réajustent. | ||
| 41 | + | ||
| 42 | +## Le fermer | ||
| 43 | + | ||
| 44 | +`Ctrl-W` appartient au shell, pas à l'éditeur : fermer un terminal se fait donc autrement. | ||
| 45 | + | ||
| 46 | +- **File ▸ Close**, ou | ||
| 47 | +- cliquez sur la case `[x]` dans son coin supérieur gauche. | ||
| 48 | + | ||
| 49 | +L'un comme l'autre terminent le shell qui y tourne. Rien n'est demandé au préalable : un terminal contient un processus en cours, pas un travail non enregistré, et fermer la fenêtre est la façon de dire que vous en avez fini. Quitter l'éditeur ferme tous les terminaux d'un coup. | ||
| 50 | + | ||
| 51 | +## Variantes | ||
| 52 | + | ||
| 53 | +- **Vous voulez lancer `golo` sans taper la commande.** Le menu **Golo** (`Alt-G`) la lance pour vous, dans une fenêtre terminal quand elle lit le clavier — le REPL, le débogueur — et dans une popup sinon. Voir [Lancer les commandes golo depuis l'éditeur](run-golo-commands.md). | ||
| 54 | +- **Vous voulez un autre shell.** Le shell est pris dans `$SHELL`, avec `/bin/sh` par défaut ; sous Windows dans `%COMSPEC%`, avec `cmd.exe` par défaut. Lancez l'éditeur avec `SHELL=/bin/zsh turbo-golo` pour le changer le temps d'une session. | ||
| 55 | +- **Aucun fichier n'est ouvert.** Le terminal démarre dans le dossier depuis lequel l'éditeur a été lancé. | ||
| 56 | +- **Vous êtes sous Windows.** Les fenêtres terminal tournent dans une pseudo-console (ConPTY), ce qui demande Windows 10 version 1809 ou plus récent, et le shell est `%COMSPEC%` — cmd.exe. Ce chemin a été compilé et vérifié mais pas encore exécuté par les auteurs, qui travaillent sous Linux et macOS. La première fois, essayez les cinq choses qu'il doit réussir — `F8`, tapez `dir`, redimensionnez la fenêtre, lancez une commande du menu qui dit `output = "terminal"`, et interrompez une commande longue avec `Ctrl-C` — et signalez ce qui ne s'est pas comporté comme attendu. | ||
| 57 | + | ||
| 58 | +## Voir aussi | ||
| 59 | + | ||
| 60 | +- Tout ce que le terminal implémente, exactement : [référence des fenêtres terminal](../reference/terminal.md) | ||
| 61 | +- Pourquoi il lance un vrai shell plutôt que de capturer la sortie d'une commande : [Fenêtres terminal](../explanation/terminal-windows.md) | ||
| 62 | +- Les couleurs qu'il utilise : [Format des fichiers de thème](../reference/themes.md) | ||
added
docs/fr/how-to/use-snippets.md +116 -0 | new file mode 100644 | ||
| @@ -0,0 +1,116 @@ | ||
| 1 | +# Insérer des snippets depuis un menu | |
| 2 | + | |
| 3 | +Ce guide montre comment mettre en place des morceaux de texte réutilisables et les insérer dans un fichier à l'endroit du curseur. Il suppose Turbo Golo déjà installé. | |
| 4 | + | |
| 5 | +## Obtenir un fichier de départ | |
| 6 | + | |
| 7 | +Lancez l'éditeur **depuis le dossier du projet**, puis choisissez **Snippets ▸ Create snippets file** (`Alt-N`, puis `C`). | |
| 8 | + | |
| 9 | +Cela écrit `.turbo-golo/snippets.toml`, rempli de douze snippets Golo — `module`, `main`, `function`, `closure`, `struct`, `union`, `augment`, `match`, `foreach`, `for`, `try`, `comprehension` — plus deux exemples d'autres groupes, et l'ouvre — coloré, puisque Turbo Golo colore le TOML : | |
| 10 | + | |
| 11 | +```toml | |
| 12 | +[[snippet]] | |
| 13 | +name = "main" | |
| 14 | +group = "Golo" | |
| 15 | +languages = ["golo"] | |
| 16 | +body = ''' | |
| 17 | +function main = |args| { | |
| 18 | + println("Hello, Golo!") | |
| 19 | +}''' | |
| 20 | + | |
| 21 | +[[snippet]] | |
| 22 | +group = "General" | |
| 23 | +name = "Hello" | |
| 24 | +body = "Hello!!!" | |
| 25 | +``` | |
| 26 | + | |
| 27 | +Chaque `[[snippet]]` devient une ligne du menu. Le fichier est relu **chaque fois que le menu s'ouvre** : une modification prend effet immédiatement, sans redémarrage. | |
| 28 | + | |
| 29 | +## En insérer un | |
| 30 | + | |
| 31 | +Ouvrez **Snippets** (`Alt-N`). Les snippets partageant un `group` apparaissent ensemble dans un sous-menu de ce nom ; celui qui n'a pas de groupe va dans **General**. | |
| 32 | + | |
| 33 | +| Touche | Effet | | |
| 34 | +| --- | --- | | |
| 35 | +| `Alt-N`, ou `F10` puis `→` jusqu'à Snippets | Ouvrir le menu | | |
| 36 | +| `↑` `↓` | Parcourir les groupes | | |
| 37 | +| `→`, ou `Entrée` | Ouvrir le groupe surligné | | |
| 38 | +| `↑` `↓` puis `Entrée` | Insérer le snippet surligné | | |
| 39 | +| `←` | Ressortir d'un groupe | | |
| 40 | +| `Échap` | Refermer tout le menu | | |
| 41 | + | |
| 42 | +Le snippet arrive au curseur. **Les lignes suivant la première sont indentées sur la ligne où vous l'avez inséré**, de sorte qu'un snippet multi-ligne déposé dans un bloc imbriqué atterrit là où vous l'auriez tapé : | |
| 43 | + | |
| 44 | +``` | |
| 45 | +function main = |args| { | |
| 46 | + | ← curseur ici | |
| 47 | +} | |
| 48 | +``` | |
| 49 | + | |
| 50 | +devient, avec le snippet `foreach`, | |
| 51 | + | |
| 52 | +``` | |
| 53 | +function main = |args| { | |
| 54 | + foreach item in list[1, 2, 3] { | |
| 55 | + println(item) | |
| 56 | + } | |
| 57 | +} | |
| 58 | +``` | |
| 59 | + | |
| 60 | +C'est une seule opération d'annulation : `Ctrl-Z` retire tout le snippet. | |
| 61 | + | |
| 62 | +## Garder ses snippets d'un projet à l'autre | |
| 63 | + | |
| 64 | +Mettez-les dans `~/.config/turbo-golo/snippets.toml` — le même dossier que vos thèmes. Ceux-là apparaissent dans tous les projets, et le fichier d'un projet s'y **ajoute** plutôt que de les remplacer. | |
| 65 | + | |
| 66 | +Quand un projet et vous employez le même `name` dans le même `group`, **celui du projet gagne** : c'est le plus spécifique des deux énoncés. | |
| 67 | + | |
| 68 | +## N'afficher un snippet que là où il a un sens | |
| 69 | + | |
| 70 | +Ajoutez `languages`, avec les noms que l'éditeur emploie — `golo`, `toml`, `markdown`, `javascript`, `html`, `bash` : | |
| 71 | + | |
| 72 | +```toml | |
| 73 | +[[snippet]] | |
| 74 | +name = "strict mode" | |
| 75 | +group = "Shell" | |
| 76 | +languages = ["bash"] | |
| 77 | +body = "set -euo pipefail" | |
| 78 | +``` | |
| 79 | + | |
| 80 | +Ce snippet n'apparaît alors que si un script shell est au premier plan. Omettez `languages` et le snippet est proposé partout, ce que vous voulez pour un en-tête de licence ou un `TODO`. | |
| 81 | + | |
| 82 | +Un groupe que le filtrage laisse vide n'apparaît pas du tout. | |
| 83 | + | |
| 84 | +## Écrire un corps Golo | |
| 85 | + | |
| 86 | +Deux habitudes du fichier de départ valent d'être reprises dans les vôtres : | |
| 87 | + | |
| 88 | +- **Deux espaces d'indentation.** C'est ce que font tous les exemples de GoloScript, et Golo n'a pas de formateur pour dire le contraire. | |
| 89 | +- **Des chaînes littérales, `'''…'''`.** Une chaîne Golo porte `\n` et `\"` ; dans une chaîne TOML basique, `"""…"""`, ces échappements seraient résolus à la lecture du fichier, avant que l'éditeur ne voie le corps. Dans une chaîne littérale, une barre oblique inverse est une barre oblique inverse, et le snippet arrive dans le fichier tel que vous l'avez écrit : | |
| 90 | + | |
| 91 | +```toml | |
| 92 | +[[snippet]] | |
| 93 | +name = "try" | |
| 94 | +group = "Golo" | |
| 95 | +languages = ["golo"] | |
| 96 | +body = ''' | |
| 97 | +try { | |
| 98 | + throw "boom" | |
| 99 | +} catch (e) { | |
| 100 | + println("caught: \"" + e + "\"") | |
| 101 | +} finally { | |
| 102 | + println("done") | |
| 103 | +}''' | |
| 104 | +``` | |
| 105 | + | |
| 106 | +## Variantes | |
| 107 | + | |
| 108 | +- **Vous avez lancé l'éditeur depuis un sous-dossier.** Le fichier du projet n'est pas trouvé : seul `./.turbo-golo` est consulté, la même règle que `settings.toml`. Vos propres snippets apparaissent quand même. | |
| 109 | +- **Le fichier contient une erreur.** Le menu affiche un `Cannot read snippets` grisé à la place des groupes, et **Create snippets file** est toujours là. Ouvrez le fichier et corrigez-le. | |
| 110 | +- **Vous voulez une tabulation dans un corps.** Dans une chaîne `"""…"""`, écrivez `\t` : le TOML le transforme en tabulation à la lecture. Dans une chaîne `'''…'''`, tapez une vraie tabulation — rien n'y est interprété. | |
| 111 | + | |
| 112 | +## Voir aussi | |
| 113 | + | |
| 114 | +- Toutes les clés du fichier et toutes les règles : [Référence des snippets](../reference/snippets.md) | |
| 115 | +- Pourquoi le menu est reconstruit à chaque ouverture, pourquoi l'insertion réindente, et pourquoi deux espaces : [Snippets](../explanation/snippets.md) | |
| 116 | +- L'autre fichier de `.turbo-golo` : [Réglages de projet](../reference/project-settings.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,116 @@ | |||
| 1 | +# Insérer des snippets depuis un menu | ||
| 2 | + | ||
| 3 | +Ce guide montre comment mettre en place des morceaux de texte réutilisables et les insérer dans un fichier à l'endroit du curseur. Il suppose Turbo Golo déjà installé. | ||
| 4 | + | ||
| 5 | +## Obtenir un fichier de départ | ||
| 6 | + | ||
| 7 | +Lancez l'éditeur **depuis le dossier du projet**, puis choisissez **Snippets ▸ Create snippets file** (`Alt-N`, puis `C`). | ||
| 8 | + | ||
| 9 | +Cela écrit `.turbo-golo/snippets.toml`, rempli de douze snippets Golo — `module`, `main`, `function`, `closure`, `struct`, `union`, `augment`, `match`, `foreach`, `for`, `try`, `comprehension` — plus deux exemples d'autres groupes, et l'ouvre — coloré, puisque Turbo Golo colore le TOML : | ||
| 10 | + | ||
| 11 | +```toml | ||
| 12 | +[[snippet]] | ||
| 13 | +name = "main" | ||
| 14 | +group = "Golo" | ||
| 15 | +languages = ["golo"] | ||
| 16 | +body = ''' | ||
| 17 | +function main = |args| { | ||
| 18 | + println("Hello, Golo!") | ||
| 19 | +}''' | ||
| 20 | + | ||
| 21 | +[[snippet]] | ||
| 22 | +group = "General" | ||
| 23 | +name = "Hello" | ||
| 24 | +body = "Hello!!!" | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +Chaque `[[snippet]]` devient une ligne du menu. Le fichier est relu **chaque fois que le menu s'ouvre** : une modification prend effet immédiatement, sans redémarrage. | ||
| 28 | + | ||
| 29 | +## En insérer un | ||
| 30 | + | ||
| 31 | +Ouvrez **Snippets** (`Alt-N`). Les snippets partageant un `group` apparaissent ensemble dans un sous-menu de ce nom ; celui qui n'a pas de groupe va dans **General**. | ||
| 32 | + | ||
| 33 | +| Touche | Effet | | ||
| 34 | +| --- | --- | | ||
| 35 | +| `Alt-N`, ou `F10` puis `→` jusqu'à Snippets | Ouvrir le menu | | ||
| 36 | +| `↑` `↓` | Parcourir les groupes | | ||
| 37 | +| `→`, ou `Entrée` | Ouvrir le groupe surligné | | ||
| 38 | +| `↑` `↓` puis `Entrée` | Insérer le snippet surligné | | ||
| 39 | +| `←` | Ressortir d'un groupe | | ||
| 40 | +| `Échap` | Refermer tout le menu | | ||
| 41 | + | ||
| 42 | +Le snippet arrive au curseur. **Les lignes suivant la première sont indentées sur la ligne où vous l'avez inséré**, de sorte qu'un snippet multi-ligne déposé dans un bloc imbriqué atterrit là où vous l'auriez tapé : | ||
| 43 | + | ||
| 44 | +``` | ||
| 45 | +function main = |args| { | ||
| 46 | + | ← curseur ici | ||
| 47 | +} | ||
| 48 | +``` | ||
| 49 | + | ||
| 50 | +devient, avec le snippet `foreach`, | ||
| 51 | + | ||
| 52 | +``` | ||
| 53 | +function main = |args| { | ||
| 54 | + foreach item in list[1, 2, 3] { | ||
| 55 | + println(item) | ||
| 56 | + } | ||
| 57 | +} | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +C'est une seule opération d'annulation : `Ctrl-Z` retire tout le snippet. | ||
| 61 | + | ||
| 62 | +## Garder ses snippets d'un projet à l'autre | ||
| 63 | + | ||
| 64 | +Mettez-les dans `~/.config/turbo-golo/snippets.toml` — le même dossier que vos thèmes. Ceux-là apparaissent dans tous les projets, et le fichier d'un projet s'y **ajoute** plutôt que de les remplacer. | ||
| 65 | + | ||
| 66 | +Quand un projet et vous employez le même `name` dans le même `group`, **celui du projet gagne** : c'est le plus spécifique des deux énoncés. | ||
| 67 | + | ||
| 68 | +## N'afficher un snippet que là où il a un sens | ||
| 69 | + | ||
| 70 | +Ajoutez `languages`, avec les noms que l'éditeur emploie — `golo`, `toml`, `markdown`, `javascript`, `html`, `bash` : | ||
| 71 | + | ||
| 72 | +```toml | ||
| 73 | +[[snippet]] | ||
| 74 | +name = "strict mode" | ||
| 75 | +group = "Shell" | ||
| 76 | +languages = ["bash"] | ||
| 77 | +body = "set -euo pipefail" | ||
| 78 | +``` | ||
| 79 | + | ||
| 80 | +Ce snippet n'apparaît alors que si un script shell est au premier plan. Omettez `languages` et le snippet est proposé partout, ce que vous voulez pour un en-tête de licence ou un `TODO`. | ||
| 81 | + | ||
| 82 | +Un groupe que le filtrage laisse vide n'apparaît pas du tout. | ||
| 83 | + | ||
| 84 | +## Écrire un corps Golo | ||
| 85 | + | ||
| 86 | +Deux habitudes du fichier de départ valent d'être reprises dans les vôtres : | ||
| 87 | + | ||
| 88 | +- **Deux espaces d'indentation.** C'est ce que font tous les exemples de GoloScript, et Golo n'a pas de formateur pour dire le contraire. | ||
| 89 | +- **Des chaînes littérales, `'''…'''`.** Une chaîne Golo porte `\n` et `\"` ; dans une chaîne TOML basique, `"""…"""`, ces échappements seraient résolus à la lecture du fichier, avant que l'éditeur ne voie le corps. Dans une chaîne littérale, une barre oblique inverse est une barre oblique inverse, et le snippet arrive dans le fichier tel que vous l'avez écrit : | ||
| 90 | + | ||
| 91 | +```toml | ||
| 92 | +[[snippet]] | ||
| 93 | +name = "try" | ||
| 94 | +group = "Golo" | ||
| 95 | +languages = ["golo"] | ||
| 96 | +body = ''' | ||
| 97 | +try { | ||
| 98 | + throw "boom" | ||
| 99 | +} catch (e) { | ||
| 100 | + println("caught: \"" + e + "\"") | ||
| 101 | +} finally { | ||
| 102 | + println("done") | ||
| 103 | +}''' | ||
| 104 | +``` | ||
| 105 | + | ||
| 106 | +## Variantes | ||
| 107 | + | ||
| 108 | +- **Vous avez lancé l'éditeur depuis un sous-dossier.** Le fichier du projet n'est pas trouvé : seul `./.turbo-golo` est consulté, la même règle que `settings.toml`. Vos propres snippets apparaissent quand même. | ||
| 109 | +- **Le fichier contient une erreur.** Le menu affiche un `Cannot read snippets` grisé à la place des groupes, et **Create snippets file** est toujours là. Ouvrez le fichier et corrigez-le. | ||
| 110 | +- **Vous voulez une tabulation dans un corps.** Dans une chaîne `"""…"""`, écrivez `\t` : le TOML le transforme en tabulation à la lecture. Dans une chaîne `'''…'''`, tapez une vraie tabulation — rien n'y est interprété. | ||
| 111 | + | ||
| 112 | +## Voir aussi | ||
| 113 | + | ||
| 114 | +- Toutes les clés du fichier et toutes les règles : [Référence des snippets](../reference/snippets.md) | ||
| 115 | +- Pourquoi le menu est reconstruit à chaque ouverture, pourquoi l'insertion réindente, et pourquoi deux espaces : [Snippets](../explanation/snippets.md) | ||
| 116 | +- L'autre fichier de `.turbo-golo` : [Réglages de projet](../reference/project-settings.md) | ||
added
docs/fr/how-to/write-a-theme.md +151 -0 | new file mode 100644 | ||
| @@ -0,0 +1,151 @@ | ||
| 1 | +# Écrire son propre thème | |
| 2 | + | |
| 3 | +Ce guide montre comment ajouter un thème de couleurs à vous. Il suppose que vous savez où se trouve votre répertoire de configuration et que vous savez éditer un fichier TOML. | |
| 4 | + | |
| 5 | +## 1. Trouver où vont les thèmes | |
| 6 | + | |
| 7 | +```bash | |
| 8 | +turbo-golo -list-themes | |
| 9 | +``` | |
| 10 | + | |
| 11 | +La dernière ligne indique le répertoire — `~/.config/turbo-golo/themes` sous Linux, `~/Library/Application Support/turbo-golo/themes` sous macOS. Créez-le : | |
| 12 | + | |
| 13 | +```bash | |
| 14 | +mkdir -p ~/.config/turbo-golo/themes | |
| 15 | +``` | |
| 16 | + | |
| 17 | +## 2. Partir d'un thème existant | |
| 18 | + | |
| 19 | +Le plus rapide est d'hériter d'un thème qui fonctionne déjà et de ne redéfinir que ce que vous voulez : | |
| 20 | + | |
| 21 | +```toml | |
| 22 | +# ~/.config/turbo-golo/themes/mine.toml | |
| 23 | +name = "Le mien" | |
| 24 | +description = "Turbo Classic, mais avec des commentaires lisibles." | |
| 25 | +inherits = "turbo-classic" | |
| 26 | + | |
| 27 | +[colors] | |
| 28 | +"syntax.comment" = { fg = "#8a8a8a", italic = true } | |
| 29 | +"syntax.string" = { fg = "#87d7af" } | |
| 30 | +``` | |
| 31 | + | |
| 32 | +Tout ce que vous ne définissez pas est repris de `turbo-classic`. | |
| 33 | + | |
| 34 | +**Héritez d'un thème dont le fond ressemble au vôtre.** Les couleurs que vous omettez ont été choisies contre le fond du thème dont vous héritez : un thème sombre bâti sur `turbo-classic` affichera, ici et là, une couleur pensée pour le marine Borland. Pour un thème sombre, héritez de `turbo-dark`, `cappuccino`, `catppuccin-frappe`, `cobalt`, `darcula` ou `monochrome-dark` ; pour un thème clair, de `borland-light`, `catppuccin-latte`, `intellij-light` ou `monochrome-light`. C'est aussi pourquoi les onze thèmes livrés dans le binaire énoncent chacun leur palette en entier au lieu d'en hériter l'essentiel — un test les y oblige, parce qu'un thème livré engage le projet. | |
| 35 | + | |
| 36 | +## 3. L'utiliser | |
| 37 | + | |
| 38 | +```bash | |
| 39 | +turbo-golo -theme mine main.golo | |
| 40 | +``` | |
| 41 | + | |
| 42 | +Ou depuis l'éditeur : `Options ▸ Theme…`, qui liste tous les thèmes trouvés. | |
| 43 | + | |
| 44 | +## 4. Itérer | |
| 45 | + | |
| 46 | +Modifiez le fichier, puis relancez l'éditeur. Il n'y a pas de rechargement à chaud. | |
| 47 | + | |
| 48 | +Si le thème ne se charge pas, Turbo Golo retombe sur le thème par défaut plutôt que de refuser de démarrer. Pour savoir *pourquoi* : | |
| 49 | + | |
| 50 | +```bash | |
| 51 | +turbo-golo -list-themes | |
| 52 | +``` | |
| 53 | + | |
| 54 | +Un thème cassé apparaît dans la liste avec l'erreur d'analyse à côté — un nom de couleur inconnu est une erreur, pas un repli silencieux : une faute de frappe est signalée au lieu de repeindre discrètement la moitié de l'écran. | |
| 55 | + | |
| 56 | +## 5. Vérifier qu'il reste lisible | |
| 57 | + | |
| 58 | +La bibliothèque soumet chaque thème qu'elle livre à cinq règles mesurées, et elles valent pour le vôtre. | |
| 59 | + | |
| 60 | +| Règle | Pourquoi | | |
| 61 | +| --- | --- | | |
| 62 | +| Le curseur est à au moins 64 de la ligne qu'il occupe, dans son canal le plus fort | Le terminal dessine son curseur par-dessus la cellule ; un curseur qui se fond est introuvable | | |
| 63 | +| Le curseur n'est jamais une simple inversion de cette ligne | Un terminal qui dessine son curseur en inversant la cellule le rendrait invisible | | |
| 64 | +| La ligne courante est à au moins 16 de la page | `turbo-dark` a un jour utilisé dix, ce qui n'est pas un surlignage | | |
| 65 | +| Le texte destiné à la lecture est à au moins 64 de son fond | Le mobilier — bureau, ombre, gouttière d'ascenseur, entrée grisée — en est exempt : il est fait pour s'effacer | | |
| 66 | +| Les commentaires se lisent à 4,5:1 ou mieux sur leur fond, en luminance relative WCAG | Un commentaire est de la prose, lue mot à mot. `turbo-classic` les dessinait en `#808080` sur son bleu marine : 128 valeurs de canal d'écart, donc la règle ci-dessus laissait passer, et 4,05:1 à la lecture, sous le plancher du W3C pour du texte courant. Les commentaires étaient la couleur la plus terne dans six des onze thèmes livrés. | | |
| 67 | + | |
| 68 | +La règle de contraste s'applique aux thèmes que le projet écrit lui-même. Les deux thèmes Catppuccin en sont exemptés, et l'exemption est écrite là où elle est faite : leurs couleurs sont la palette publiée de quelqu'un d'autre, copiée fidèlement, et Catppuccin place les commentaires à 2,87:1 en Frappé et 2,83:1 en Latte. Un thème nommé Catppuccin qui n'aurait pas exactement ces valeurs serait un autre thème portant un nom d'emprunt : le correctif, si quelqu'un en veut un, est en amont. | |
| 69 | + | |
| 70 | +Elle s'applique aux commentaires et à rien d'autre. `syntax.punctuation` est plus discret encore dans plusieurs thèmes et le reste : la ponctuation se reconnaît à sa forme, elle ne se lit pas. | |
| 71 | + | |
| 72 | +Une sixième règle attrape l'erreur qu'aucune mesure ne voit : **deux classes syntaxiques que le lecteur rencontre côte à côte ne doivent pas être dessinées à l'identique**. `turbo-classic` a un jour peint `syntax.link` du même vert que `syntax.string` : un lien Markdown et un extrait de code inline devenaient la même chose à l'écran — chaque couleur lisible, chaque clé définie, et les deux simplement égales. Les deux monochromes satisfont cette règle sans aucune teinte, en jouant du gras, de l'italique et du souligné. | |
| 73 | + | |
| 74 | +## Variantes | |
| 75 | + | |
| 76 | +**Remplacer un thème livré plutôt que d'en ajouter un.** Donnez à votre fichier le même nom — `turbo-classic.toml` — et c'est le vôtre qui gagne. Le thème embarqué n'est pas remplacé : supprimer votre fichier le fait revenir. | |
| 77 | + | |
| 78 | +**Partir de zéro.** N'écrivez pas `inherits`. Définissez au moins `default` ; toute clé non définie retombe le long des points jusqu'à lui, si bien qu'un thème d'une seule ligne reste un thème utilisable. | |
| 79 | + | |
| 80 | +**Ne colorer que la syntaxe.** Une seule clé suffit : | |
| 81 | + | |
| 82 | +```toml | |
| 83 | +[colors] | |
| 84 | +syntax = { fg = "silver" } | |
| 85 | +``` | |
| 86 | + | |
| 87 | +`syntax.keyword`, `syntax.string` et les autres y retombent toutes. | |
| 88 | + | |
| 89 | +**Garder les couleurs du terminal.** Utilisez `default` comme valeur : | |
| 90 | + | |
| 91 | +```toml | |
| 92 | +[colors] | |
| 93 | +"editor.text" = { fg = "default", bg = "default" } | |
| 94 | +``` | |
| 95 | + | |
| 96 | +**L'essayer depuis un clone sans l'installer.** Pointez l'éditeur sur n'importe quel répertoire : | |
| 97 | + | |
| 98 | +```bash | |
| 99 | +TURBO_GOLO_THEME_DIR=./mes-themes turbo-golo -theme mine main.golo | |
| 100 | +``` | |
| 101 | + | |
| 102 | +**Le curseur est peu visible.** `editor.cursor` fait deux choses : son **fond** est envoyé au terminal comme couleur de son propre curseur, et il peint aussi la cellule en dessous, en secours pour les terminaux qui ignorent la première. Mettez-la sur quelque chose de criard : | |
| 103 | + | |
| 104 | +```toml | |
| 105 | +[colors] | |
| 106 | +"editor.cursor" = { fg = "#000000", bg = "#ff8700" } | |
| 107 | +``` | |
| 108 | + | |
| 109 | +Deux choses font une mauvaise couleur de curseur, et la suite de tests de la bibliothèque les refuse toutes les deux : une simple inversion de la ligne — que les terminaux dessinant leur curseur par inversion ramènent à l'invisibilité — et tout écart de moins de 64 valeurs de canal avec la ligne sur laquelle il se trouve. | |
| 110 | + | |
| 111 | +**La ligne du curseur est difficile à repérer.** C'est `editor.currentline`, une autre clé. Elle doit s'écarter de `editor.text` d'au moins 16 valeurs de canal pour être un surlignage. | |
| 112 | + | |
| 113 | +**Le Markdown et le HTML rendent fade.** Cinq clés appartiennent aux langages de balisage et n'ont pas d'équivalent en Golo : un thème écrit avant leur existence ne les définit pas. | |
| 114 | + | |
| 115 | +```toml | |
| 116 | +[colors] | |
| 117 | +"syntax.heading" = { fg = "white", bold = true } | |
| 118 | +"syntax.tag" = { fg = "aqua" } | |
| 119 | +"syntax.attribute" = { fg = "yellow" } | |
| 120 | +"syntax.emphasis" = { fg = "fuchsia", bold = true } | |
| 121 | +"syntax.link" = { fg = "aqua", underline = true } | |
| 122 | +``` | |
| 123 | + | |
| 124 | +Donnez à `syntax.link` une couleur différente de `syntax.string` : un lien et un `code` en ligne se côtoient dans presque toute prose, et partager une couleur en fait une bouillie. Le `turbo-classic` livré avait exactement ce défaut jusqu'à ce qu'on le regarde sur un vrai terminal. | |
| 125 | + | |
| 126 | +**L'arbre du projet est plat.** Il a quatre clés à lui, dont aucune ne se rabat sur `list` : | |
| 127 | + | |
| 128 | +```toml | |
| 129 | +[colors] | |
| 130 | +"tree.text" = { fg = "silver", bg = "navy" } | |
| 131 | +"tree.directory" = { fg = "white", bg = "navy", bold = true } | |
| 132 | +"tree.selected" = { fg = "black", bg = "aqua" } | |
| 133 | +"tree.unfocused" = { fg = "black", bg = "gray" } | |
| 134 | +``` | |
| 135 | + | |
| 136 | +Donnez à `tree.text` le même fond que `window.body`, pour que l'arbre fasse partie de sa fenêtre, et rendez `tree.selected` nettement différent — la suite de tests tient chaque thème livré à au moins 64 valeurs de canal entre les deux, parce qu'un surlignage de la couleur de la page n'est pas un surlignage. | |
| 137 | + | |
| 138 | +**Les fenêtres terminal rendent mal.** Elles ont deux clés à elles, et aucune ne se rabat sur `editor` : | |
| 139 | + | |
| 140 | +```toml | |
| 141 | +[colors] | |
| 142 | +"terminal.text" = { fg = "silver", bg = "black" } | |
| 143 | +"terminal.cursor" = { fg = "black", bg = "aqua" } | |
| 144 | +``` | |
| 145 | + | |
| 146 | +`terminal.text` est ce que reçoit la sortie d'un shell lorsqu'elle ne nomme aucune couleur — donnez-lui quelque chose de proche d'un vrai terminal plutôt que du fond de votre éditeur, sans quoi `less` et `htop` détonneront. Un programme qui nomme ses couleurs les conserve dans les deux cas. | |
| 147 | + | |
| 148 | +## Voir aussi | |
| 149 | + | |
| 150 | +- Toutes les clés définissables et tous les noms de couleurs : [référence du format de thème](../reference/themes.md) | |
| 151 | +- Pourquoi du TOML avec deux formes d'héritage : [Décisions de conception](../explanation/design-decisions.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,151 @@ | |||
| 1 | +# Écrire son propre thème | ||
| 2 | + | ||
| 3 | +Ce guide montre comment ajouter un thème de couleurs à vous. Il suppose que vous savez où se trouve votre répertoire de configuration et que vous savez éditer un fichier TOML. | ||
| 4 | + | ||
| 5 | +## 1. Trouver où vont les thèmes | ||
| 6 | + | ||
| 7 | +```bash | ||
| 8 | +turbo-golo -list-themes | ||
| 9 | +``` | ||
| 10 | + | ||
| 11 | +La dernière ligne indique le répertoire — `~/.config/turbo-golo/themes` sous Linux, `~/Library/Application Support/turbo-golo/themes` sous macOS. Créez-le : | ||
| 12 | + | ||
| 13 | +```bash | ||
| 14 | +mkdir -p ~/.config/turbo-golo/themes | ||
| 15 | +``` | ||
| 16 | + | ||
| 17 | +## 2. Partir d'un thème existant | ||
| 18 | + | ||
| 19 | +Le plus rapide est d'hériter d'un thème qui fonctionne déjà et de ne redéfinir que ce que vous voulez : | ||
| 20 | + | ||
| 21 | +```toml | ||
| 22 | +# ~/.config/turbo-golo/themes/mine.toml | ||
| 23 | +name = "Le mien" | ||
| 24 | +description = "Turbo Classic, mais avec des commentaires lisibles." | ||
| 25 | +inherits = "turbo-classic" | ||
| 26 | + | ||
| 27 | +[colors] | ||
| 28 | +"syntax.comment" = { fg = "#8a8a8a", italic = true } | ||
| 29 | +"syntax.string" = { fg = "#87d7af" } | ||
| 30 | +``` | ||
| 31 | + | ||
| 32 | +Tout ce que vous ne définissez pas est repris de `turbo-classic`. | ||
| 33 | + | ||
| 34 | +**Héritez d'un thème dont le fond ressemble au vôtre.** Les couleurs que vous omettez ont été choisies contre le fond du thème dont vous héritez : un thème sombre bâti sur `turbo-classic` affichera, ici et là, une couleur pensée pour le marine Borland. Pour un thème sombre, héritez de `turbo-dark`, `cappuccino`, `catppuccin-frappe`, `cobalt`, `darcula` ou `monochrome-dark` ; pour un thème clair, de `borland-light`, `catppuccin-latte`, `intellij-light` ou `monochrome-light`. C'est aussi pourquoi les onze thèmes livrés dans le binaire énoncent chacun leur palette en entier au lieu d'en hériter l'essentiel — un test les y oblige, parce qu'un thème livré engage le projet. | ||
| 35 | + | ||
| 36 | +## 3. L'utiliser | ||
| 37 | + | ||
| 38 | +```bash | ||
| 39 | +turbo-golo -theme mine main.golo | ||
| 40 | +``` | ||
| 41 | + | ||
| 42 | +Ou depuis l'éditeur : `Options ▸ Theme…`, qui liste tous les thèmes trouvés. | ||
| 43 | + | ||
| 44 | +## 4. Itérer | ||
| 45 | + | ||
| 46 | +Modifiez le fichier, puis relancez l'éditeur. Il n'y a pas de rechargement à chaud. | ||
| 47 | + | ||
| 48 | +Si le thème ne se charge pas, Turbo Golo retombe sur le thème par défaut plutôt que de refuser de démarrer. Pour savoir *pourquoi* : | ||
| 49 | + | ||
| 50 | +```bash | ||
| 51 | +turbo-golo -list-themes | ||
| 52 | +``` | ||
| 53 | + | ||
| 54 | +Un thème cassé apparaît dans la liste avec l'erreur d'analyse à côté — un nom de couleur inconnu est une erreur, pas un repli silencieux : une faute de frappe est signalée au lieu de repeindre discrètement la moitié de l'écran. | ||
| 55 | + | ||
| 56 | +## 5. Vérifier qu'il reste lisible | ||
| 57 | + | ||
| 58 | +La bibliothèque soumet chaque thème qu'elle livre à cinq règles mesurées, et elles valent pour le vôtre. | ||
| 59 | + | ||
| 60 | +| Règle | Pourquoi | | ||
| 61 | +| --- | --- | | ||
| 62 | +| Le curseur est à au moins 64 de la ligne qu'il occupe, dans son canal le plus fort | Le terminal dessine son curseur par-dessus la cellule ; un curseur qui se fond est introuvable | | ||
| 63 | +| Le curseur n'est jamais une simple inversion de cette ligne | Un terminal qui dessine son curseur en inversant la cellule le rendrait invisible | | ||
| 64 | +| La ligne courante est à au moins 16 de la page | `turbo-dark` a un jour utilisé dix, ce qui n'est pas un surlignage | | ||
| 65 | +| Le texte destiné à la lecture est à au moins 64 de son fond | Le mobilier — bureau, ombre, gouttière d'ascenseur, entrée grisée — en est exempt : il est fait pour s'effacer | | ||
| 66 | +| Les commentaires se lisent à 4,5:1 ou mieux sur leur fond, en luminance relative WCAG | Un commentaire est de la prose, lue mot à mot. `turbo-classic` les dessinait en `#808080` sur son bleu marine : 128 valeurs de canal d'écart, donc la règle ci-dessus laissait passer, et 4,05:1 à la lecture, sous le plancher du W3C pour du texte courant. Les commentaires étaient la couleur la plus terne dans six des onze thèmes livrés. | | ||
| 67 | + | ||
| 68 | +La règle de contraste s'applique aux thèmes que le projet écrit lui-même. Les deux thèmes Catppuccin en sont exemptés, et l'exemption est écrite là où elle est faite : leurs couleurs sont la palette publiée de quelqu'un d'autre, copiée fidèlement, et Catppuccin place les commentaires à 2,87:1 en Frappé et 2,83:1 en Latte. Un thème nommé Catppuccin qui n'aurait pas exactement ces valeurs serait un autre thème portant un nom d'emprunt : le correctif, si quelqu'un en veut un, est en amont. | ||
| 69 | + | ||
| 70 | +Elle s'applique aux commentaires et à rien d'autre. `syntax.punctuation` est plus discret encore dans plusieurs thèmes et le reste : la ponctuation se reconnaît à sa forme, elle ne se lit pas. | ||
| 71 | + | ||
| 72 | +Une sixième règle attrape l'erreur qu'aucune mesure ne voit : **deux classes syntaxiques que le lecteur rencontre côte à côte ne doivent pas être dessinées à l'identique**. `turbo-classic` a un jour peint `syntax.link` du même vert que `syntax.string` : un lien Markdown et un extrait de code inline devenaient la même chose à l'écran — chaque couleur lisible, chaque clé définie, et les deux simplement égales. Les deux monochromes satisfont cette règle sans aucune teinte, en jouant du gras, de l'italique et du souligné. | ||
| 73 | + | ||
| 74 | +## Variantes | ||
| 75 | + | ||
| 76 | +**Remplacer un thème livré plutôt que d'en ajouter un.** Donnez à votre fichier le même nom — `turbo-classic.toml` — et c'est le vôtre qui gagne. Le thème embarqué n'est pas remplacé : supprimer votre fichier le fait revenir. | ||
| 77 | + | ||
| 78 | +**Partir de zéro.** N'écrivez pas `inherits`. Définissez au moins `default` ; toute clé non définie retombe le long des points jusqu'à lui, si bien qu'un thème d'une seule ligne reste un thème utilisable. | ||
| 79 | + | ||
| 80 | +**Ne colorer que la syntaxe.** Une seule clé suffit : | ||
| 81 | + | ||
| 82 | +```toml | ||
| 83 | +[colors] | ||
| 84 | +syntax = { fg = "silver" } | ||
| 85 | +``` | ||
| 86 | + | ||
| 87 | +`syntax.keyword`, `syntax.string` et les autres y retombent toutes. | ||
| 88 | + | ||
| 89 | +**Garder les couleurs du terminal.** Utilisez `default` comme valeur : | ||
| 90 | + | ||
| 91 | +```toml | ||
| 92 | +[colors] | ||
| 93 | +"editor.text" = { fg = "default", bg = "default" } | ||
| 94 | +``` | ||
| 95 | + | ||
| 96 | +**L'essayer depuis un clone sans l'installer.** Pointez l'éditeur sur n'importe quel répertoire : | ||
| 97 | + | ||
| 98 | +```bash | ||
| 99 | +TURBO_GOLO_THEME_DIR=./mes-themes turbo-golo -theme mine main.golo | ||
| 100 | +``` | ||
| 101 | + | ||
| 102 | +**Le curseur est peu visible.** `editor.cursor` fait deux choses : son **fond** est envoyé au terminal comme couleur de son propre curseur, et il peint aussi la cellule en dessous, en secours pour les terminaux qui ignorent la première. Mettez-la sur quelque chose de criard : | ||
| 103 | + | ||
| 104 | +```toml | ||
| 105 | +[colors] | ||
| 106 | +"editor.cursor" = { fg = "#000000", bg = "#ff8700" } | ||
| 107 | +``` | ||
| 108 | + | ||
| 109 | +Deux choses font une mauvaise couleur de curseur, et la suite de tests de la bibliothèque les refuse toutes les deux : une simple inversion de la ligne — que les terminaux dessinant leur curseur par inversion ramènent à l'invisibilité — et tout écart de moins de 64 valeurs de canal avec la ligne sur laquelle il se trouve. | ||
| 110 | + | ||
| 111 | +**La ligne du curseur est difficile à repérer.** C'est `editor.currentline`, une autre clé. Elle doit s'écarter de `editor.text` d'au moins 16 valeurs de canal pour être un surlignage. | ||
| 112 | + | ||
| 113 | +**Le Markdown et le HTML rendent fade.** Cinq clés appartiennent aux langages de balisage et n'ont pas d'équivalent en Golo : un thème écrit avant leur existence ne les définit pas. | ||
| 114 | + | ||
| 115 | +```toml | ||
| 116 | +[colors] | ||
| 117 | +"syntax.heading" = { fg = "white", bold = true } | ||
| 118 | +"syntax.tag" = { fg = "aqua" } | ||
| 119 | +"syntax.attribute" = { fg = "yellow" } | ||
| 120 | +"syntax.emphasis" = { fg = "fuchsia", bold = true } | ||
| 121 | +"syntax.link" = { fg = "aqua", underline = true } | ||
| 122 | +``` | ||
| 123 | + | ||
| 124 | +Donnez à `syntax.link` une couleur différente de `syntax.string` : un lien et un `code` en ligne se côtoient dans presque toute prose, et partager une couleur en fait une bouillie. Le `turbo-classic` livré avait exactement ce défaut jusqu'à ce qu'on le regarde sur un vrai terminal. | ||
| 125 | + | ||
| 126 | +**L'arbre du projet est plat.** Il a quatre clés à lui, dont aucune ne se rabat sur `list` : | ||
| 127 | + | ||
| 128 | +```toml | ||
| 129 | +[colors] | ||
| 130 | +"tree.text" = { fg = "silver", bg = "navy" } | ||
| 131 | +"tree.directory" = { fg = "white", bg = "navy", bold = true } | ||
| 132 | +"tree.selected" = { fg = "black", bg = "aqua" } | ||
| 133 | +"tree.unfocused" = { fg = "black", bg = "gray" } | ||
| 134 | +``` | ||
| 135 | + | ||
| 136 | +Donnez à `tree.text` le même fond que `window.body`, pour que l'arbre fasse partie de sa fenêtre, et rendez `tree.selected` nettement différent — la suite de tests tient chaque thème livré à au moins 64 valeurs de canal entre les deux, parce qu'un surlignage de la couleur de la page n'est pas un surlignage. | ||
| 137 | + | ||
| 138 | +**Les fenêtres terminal rendent mal.** Elles ont deux clés à elles, et aucune ne se rabat sur `editor` : | ||
| 139 | + | ||
| 140 | +```toml | ||
| 141 | +[colors] | ||
| 142 | +"terminal.text" = { fg = "silver", bg = "black" } | ||
| 143 | +"terminal.cursor" = { fg = "black", bg = "aqua" } | ||
| 144 | +``` | ||
| 145 | + | ||
| 146 | +`terminal.text` est ce que reçoit la sortie d'un shell lorsqu'elle ne nomme aucune couleur — donnez-lui quelque chose de proche d'un vrai terminal plutôt que du fond de votre éditeur, sans quoi `less` et `htop` détonneront. Un programme qui nomme ses couleurs les conserve dans les deux cas. | ||
| 147 | + | ||
| 148 | +## Voir aussi | ||
| 149 | + | ||
| 150 | +- Toutes les clés définissables et tous les noms de couleurs : [référence du format de thème](../reference/themes.md) | ||
| 151 | +- Pourquoi du TOML avec deux formes d'héritage : [Décisions de conception](../explanation/design-decisions.md) | ||
added
docs/fr/reference/acp.md +239 -0 | new file mode 100644 | ||
| @@ -0,0 +1,239 @@ | ||
| 1 | +# Agents et ACP | |
| 2 | + | |
| 3 | +Turbo Golo est un client de l'[Agent Client Protocol](https://agentclientprotocol.com). Il lance chaque agent comme processus fils et échange avec lui des messages JSON-RPC 2.0 sur son entrée et sa sortie standard, à raison d'un message par ligne. | |
| 4 | + | |
| 5 | +## Où se trouve le fichier | |
| 6 | + | |
| 7 | +| Chemin | Lu | Rôle | | |
| 8 | +| --- | --- | --- | | |
| 9 | +| `~/.config/turbo-golo/acp.toml` | en premier | Les agents que vous voulez dans tous les projets | | |
| 10 | +| `<projet>/.turbo-gololo/acp.toml` | en second | Les agents propres à ce projet | | |
| 11 | + | |
| 12 | +Les deux sont facultatifs. Quand un `name` d'agent apparaît dans les deux, celui du projet remplace celui de l'utilisateur, étant l'énoncé le plus spécifique — la règle que suivent déjà les [snippets](snippets.md). Un fichier absent n'est pas une erreur ; un fichier présent mais illisible en est une, signalée sous **Agent ▸ Agent status** plutôt que de laisser silencieusement le menu vide. | |
| 13 | + | |
| 14 | +`TURBO_GOLO_DIR` remplace le dossier où le fichier utilisateur est cherché. Le fichier du projet est toujours `.turbo-gololo/acp.toml` sous le dossier depuis lequel l'éditeur a été lancé — il n'y a pas de remontée dans l'arborescence, pour la même raison que les [réglages de projet](project-settings.md) ne remontent pas. | |
| 15 | + | |
| 16 | +## Format du fichier | |
| 17 | + | |
| 18 | +Un bloc `[[agent]]` par agent, dans l'ordre souhaité dans le menu. | |
| 19 | + | |
| 20 | +```toml | |
| 21 | +[[agent]] | |
| 22 | +name = "Bob (llama.cpp)" | |
| 23 | +command = "docker" | |
| 24 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | |
| 25 | +env = { TELEMETRY_ENABLED = "false" } | |
| 26 | +cwd = "." | |
| 27 | +``` | |
| 28 | + | |
| 29 | +| Clé | Type | Requise | Signification | | |
| 30 | +| --- | --- | --- | --- | | |
| 31 | +| `name` | chaîne | **oui** | Ce qu'affiche le menu Agent et le titre de la fenêtre. Doit être unique dans l'ensemble fusionné. | | |
| 32 | +| `command` | chaîne | **oui** | L'exécutable à lancer. Cherché dans `PATH` sauf s'il contient un séparateur. | | |
| 33 | +| `args` | liste de chaînes | non | Ses arguments, passés tels quels — pas de shell, donc ni guillemets, ni jokers, ni `&&`. | | |
| 34 | +| `env` | table de chaînes | non | Variables d'environnement ajoutées à celles de l'éditeur. Un nom donné ici l'emporte. | | |
| 35 | +| `cwd` | chaîne | non | Où le processus démarre, et le `cwd` annoncé à l'agent. Relatif à la racine du projet. Par défaut, la racine du projet. | | |
| 36 | + | |
| 37 | +`env` peut aussi s'écrire en sous-table, ce qui est la même chose : | |
| 38 | + | |
| 39 | +```toml | |
| 40 | +[[agent]] | |
| 41 | +name = "Bob (llama.cpp)" | |
| 42 | +command = "docker" | |
| 43 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | |
| 44 | + | |
| 45 | +[agent.env] | |
| 46 | +TELEMETRY_ENABLED = "false" | |
| 47 | +``` | |
| 48 | + | |
| 49 | +### Ce qui est refusé | |
| 50 | + | |
| 51 | +Le fichier est refusé dans son ensemble, plutôt que chargé à moitié, dès que l'un de ces cas se présente. Un menu à moitié chargé proposant trois de vos cinq agents est pire qu'une erreur qui dit pourquoi. | |
| 52 | + | |
| 53 | +| Problème | Message | | |
| 54 | +| --- | --- | | |
| 55 | +| un agent sans `name` | `reading …/acp.toml: agent 1 has no name` | | |
| 56 | +| un agent sans `command` | `reading …/acp.toml: agent "Bob" has no command` | | |
| 57 | +| deux agents portant le même `name` | `reading …/acp.toml: two agents are called "Bob"` | | |
| 58 | +| une clé que le format ne définit pas | `reading …/acp.toml: agent.comand is not a key this file has` | | |
| 59 | + | |
| 60 | +Le dernier cas est voulu : une clé mal orthographiée silencieusement ignorée ressemblerait exactement à une clé sans effet. | |
| 61 | + | |
| 62 | +## Le menu Agent | |
| 63 | + | |
| 64 | +`Alt-A` l'ouvre. Il est sur la barre qu'un agent soit configuré ou non, parce que c'est de là que **Create agents file** doit être atteignable. | |
| 65 | + | |
| 66 | +| Entrée | Active quand | Effet | | |
| 67 | +| --- | --- | --- | | |
| 68 | +| *une entrée par agent, par son nom* | toujours | Démarrer cet agent et ouvrir une fenêtre dessus | | |
| 69 | +| **Create agents file** | pas d'`acp.toml` dans le projet | Écrire le fichier de départ et l'ouvrir | | |
| 70 | +| **Cancel turn** | un tour est en cours dans la fenêtre de devant | `session/cancel` | | |
| 71 | +| **Agent status** | toujours | Ce qui a été chargé, la ligne de commande de chacun, et ce qui a échoué | | |
| 72 | + | |
| 73 | +## Touches dans une fenêtre agent | |
| 74 | + | |
| 75 | +Une fenêtre agent est une fenêtre ordinaire : `F6`, `Alt-1`…`Alt-9`, Tile, Maximise, `[x]` et `[■]` y fonctionnent tous. À l'intérieur : | |
| 76 | + | |
| 77 | +| Touche | Effet | | |
| 78 | +| --- | --- | | |
| 79 | +| `Entrée` | Envoyer la zone de saisie comme invite | | |
| 80 | +| `Alt-Entrée` | Insérer un saut de ligne dans la zone de saisie | | |
| 81 | +| `Tab` | Déplacer le focus entre la conversation et la zone de saisie | | |
| 82 | +| `Ctrl-C`, `Ctrl-Ins` | Copier la sélection, ou le bloc sur lequel est le curseur | | |
| 83 | +| `Échap` | Abandonner la sélection ; s'il n'y en a pas, annuler le tour en cours | | |
| 84 | +| `Ctrl-W` | Fermer la fenêtre et arrêter l'agent | | |
| 85 | + | |
| 86 | +Avec la **zone de saisie** au premier plan : | |
| 87 | + | |
| 88 | +| Touche | Effet | | |
| 89 | +| --- | --- | | |
| 90 | +| `↑` `↓` `←` `→` `Début` `Fin` | Déplacer le curseur dans ce que vous tapez | | |
| 91 | +| `Retour arrière` `Suppr` | L'éditer ; le retour arrière en début de ligne la joint à celle du dessus | | |
| 92 | +| `/` en premier caractère | Ouvrir la liste des commandes de l'agent — voir [Commandes et mentions](#commandes-et-mentions) | | |
| 93 | +| `@` | Ouvrir la liste des fichiers du projet, réduite par ce que vous tapez ensuite | | |
| 94 | +| `↑` `↓` `PgUp` `PgDn`, liste ouverte | Se déplacer dans la liste | | |
| 95 | +| `Tab`, liste ouverte | Prendre l'entrée en surbrillance | | |
| 96 | +| `Entrée`, liste ouverte | Prendre l'entrée en surbrillance ; sur un mot déjà complet, envoyer | | |
| 97 | +| `Échap`, liste ouverte | Fermer la liste jusqu'à ce que le texte change | | |
| 98 | + | |
| 99 | +Avec la **conversation** au premier plan : | |
| 100 | + | |
| 101 | +| Touche | Effet | | |
| 102 | +| --- | --- | | |
| 103 | +| `↑` `↓` | Déplacer le curseur d'une ligne | | |
| 104 | +| `PgUp` `PgDn` | Le déplacer d'un écran | | |
| 105 | +| `Début` `Fin` | Le début de la conversation, et la fin | | |
| 106 | +| `Shift-` l'une d'elles | Étendre la sélection à la place | | |
| 107 | +| Glisser avec le bouton 1 | Sélectionner à la main | | |
| 108 | +| Molette | Défiler de trois lignes sans bouger le curseur | | |
| 109 | + | |
| 110 | +Contrairement à une fenêtre terminal, une fenêtre agent ne **prend pas** les raccourcis de l'éditeur : il n'y a pas de shell qui ait besoin de `Ctrl-F`, donc cette touche garde son sens habituel. `Ctrl-C` fait exception, et seulement parce que rien d'autre n'en veut dans une fenêtre agent. | |
| 111 | + | |
| 112 | +## Commandes et mentions | |
| 113 | + | |
| 114 | +Deux caractères ouvrent une liste par-dessus le bas de la conversation pendant que vous tapez. Ce sont les deux mêmes que Zed, si bien que la documentation d'un agent — « tapez `/web` pour chercher » — reste vraie ici. | |
| 115 | + | |
| 116 | +### `/` — les commandes de l'agent | |
| 117 | + | |
| 118 | +Un agent peut annoncer des commandes par `available_commands_update`, au début de la session ou à tout moment pendant celle-ci. Taper `/` comme **premier caractère** de la zone de saisie les liste : le nom, la description donnée par l'agent et, entre chevrons, ce qu'il attend après le nom quand il attend quelque chose. Continuez à taper pour réduire la liste ; la correspondance porte sur le début du nom et ignore la casse. | |
| 119 | + | |
| 120 | +`Tab` complète la commande en surbrillance. Une commande qui prend une entrée est complétée avec une espace à la fin, pour que la suite de votre frappe soit son argument ; une qui n'en prend pas est complétée au nom seul. `Entrée` complète aussi, sauf sur un mot qui se lit déjà exactement comme une commande, où elle envoie. | |
| 121 | + | |
| 122 | +Sur le fil, une commande est du **texte** : `/web agent client protocol` part comme un seul bloc texte, et l'agent la reconnaît à son premier mot. C'est tout le protocole des commandes, et c'est pourquoi un `/` ailleurs qu'au début de la zone n'est qu'un caractère. | |
| 123 | + | |
| 124 | +Sans commande annoncée, `/` est un caractère et `Tab` garde son sens habituel. **Agent ▸ Agent status** liste les commandes avec leur description. | |
| 125 | + | |
| 126 | +### `@` — un fichier du projet | |
| 127 | + | |
| 128 | +Taper `@` n'importe où dans la zone liste les fichiers du projet, relatifs à sa racine, avec des barres obliques. Ce que vous tapez après le `@` réduit la liste : les fichiers dont le nom propre commence par cela viennent d'abord, puis ceux dont le chemin le contient seulement. `Tab` ou `Entrée` complète celui en surbrillance et ajoute une espace. | |
| 129 | + | |
| 130 | +À l'envoi de l'invite, chaque `@nom` qui désigne un fichier connu de la liste devient un bloc de contenu **à la place du nom** : | |
| 131 | + | |
| 132 | +| L'agent a déclaré | Le bloc envoyé | | |
| 133 | +| --- | --- | | |
| 134 | +| `promptCapabilities.embeddedContext: true` | `resource` — l'`uri` du fichier, son `mimeType` et son `text` entier, lu comme `fs/read_text_file` le lit : depuis le tampon ouvert quand le fichier est ouvert et modifié | | |
| 135 | +| autre chose, ou le fichier n'a pas pu être lu | `resource_link` — l'`uri`, le `name` et le `mimeType`, pour que l'agent aille le chercher lui-même | | |
| 136 | + | |
| 137 | +Les mots de part et d'autre partent en blocs texte, si bien que `explique @docs/README.md s'il te plaît` fait trois blocs : `explique `, le fichier, ` s'il te plaît`. La conversation garde la ligne telle que vous l'avez tapée. | |
| 138 | + | |
| 139 | +Un mot qui commence par `@` et ne désigne aucun fichier reste du texte — une adresse électronique dans une invite n'est pas un fichier — et `@main.go` ne désigne pas `main.gopher` : le nom doit terminer le mot. | |
| 140 | + | |
| 141 | +La liste est le projet parcouru depuis sa racine, `.git` exclu, au plus 5 000 fichiers, et au plus 200 d'entre eux affichés à la fois. Au-delà de l'une ou l'autre limite, tapez une lettre de plus. Le parcours est refait à chaque ouverture de la liste par `@`, si bien qu'un fichier que l'agent vient de créer y figure. | |
| 142 | + | |
| 143 | +## La copie | |
| 144 | + | |
| 145 | +La sélection porte sur des **lignes entières**. Rien ne s'édite dans une conversation, donc une demi-ligne n'est jamais ce qu'on veut dire, et des lignes entières préservent l'indentation d'un bloc de code copié. | |
| 146 | + | |
| 147 | +Sans rien de sélectionné, la copie prend la **région sur laquelle est le curseur** : un bloc de code délimité, un passage de prose, la sortie d'un appel d'outil. Le libellé d'un interlocuteur et l'en-tête d'un appel d'outil sont du mobilier et forment des régions à part : ni l'un ni l'autre n'est jamais copié avec ce qu'il surmonte. | |
| 148 | + | |
| 149 | +L'indentation d'affichage de la conversation est retirée, donc le code collé arrive collé à la marge. | |
| 150 | + | |
| 151 | +Le texte part à deux endroits à la fois : | |
| 152 | + | |
| 153 | +| Presse-papiers | Comment | Collé avec | | |
| 154 | +| --- | --- | --- | | |
| 155 | +| Celui de l'éditeur | directement | `Shift-Ins`, dans un fichier ouvert ici | | |
| 156 | +| Celui du système | OSC 52, à travers le terminal | `Ctrl-V`, n'importe où ailleurs | | |
| 157 | + | |
| 158 | +Rien ne vérifie que le terminal a accepté le second : il n'y a pas de réponse à vérifier, et un terminal peut refuser OSC 52 par sécurité ou demander qu'on l'active. Le presse-papiers de l'éditeur contient le texte dans tous les cas, et la barre d'état dit combien de lignes ont été copiées. | |
| 159 | + | |
| 160 | +## Quelle part du protocole est implémentée | |
| 161 | + | |
| 162 | +Version de protocole **1**. Turbo Golo annonce sa version dans `initialize` et accepte la version que l'agent répond, pourvu qu'il la connaisse. | |
| 163 | + | |
| 164 | +### Ce que l'éditeur appelle sur l'agent | |
| 165 | + | |
| 166 | +| Méthode | Implémentée | Remarques | | |
| 167 | +| --- | --- | --- | | |
| 168 | +| `initialize` | oui | Annonce la capacité `fs` ci-dessous ; `terminal` n'est pas annoncée | | |
| 169 | +| `session/new` | oui | `cwd` vient de la clé `cwd` de l'agent ; `mcpServers` est toujours vide — les serveurs MCP sont l'affaire de l'agent | | |
| 170 | +| `session/prompt` | oui | Des blocs texte, et un bloc `resource` ou `resource_link` par fichier désigné par `@` — voir [Commandes et mentions](#commandes-et-mentions) | | |
| 171 | +| `session/cancel` | oui | `Échap`, et **Agent ▸ Cancel turn** | | |
| 172 | +| `session/load` | **non** | Les conversations ne survivent pas à la fermeture de la fenêtre | | |
| 173 | +| `authenticate` | **non** | Un agent qui liste des `authMethods` est signalé comme exigeant une connexion que l'éditeur ne sait pas faire | | |
| 174 | + | |
| 175 | +### Ce que l'agent peut appeler sur l'éditeur | |
| 176 | + | |
| 177 | +| Méthode | Implémentée | Remarques | | |
| 178 | +| --- | --- | --- | | |
| 179 | +| `session/update` | oui | Voir la table ci-dessous | | |
| 180 | +| `session/request_permission` | oui | Une boîte modale portant les options de l'agent lui-même | | |
| 181 | +| `fs/read_text_file` | oui | Depuis le tampon quand le fichier est ouvert et modifié, sinon depuis le disque | | |
| 182 | +| `fs/write_text_file` | oui | Dans le tampon quand le fichier est ouvert, sinon sur le disque | | |
| 183 | +| `terminal/*` | **non** | Non annoncée, donc un agent conforme ne la demandera pas | | |
| 184 | + | |
| 185 | +### Mises à jour de session | |
| 186 | + | |
| 187 | +| `sessionUpdate` | Affiché comme | | |
| 188 | +| --- | --- | | |
| 189 | +| `agent_message_chunk` | La réponse de l'agent, ajoutée au fil de son arrivée | | |
| 190 | +| `agent_thought_chunk` | La même chose, dans la couleur des commentaires, sous une étiquette *réflexion* | | |
| 191 | +| `user_message_chunk` | Votre propre message, tel que l'agent le renvoie | | |
| 192 | +| `tool_call` | Une ligne nommant l'outil et son titre, avec son état | | |
| 193 | +| `tool_call_update` | Repliée sur la ligne dont le `toolCallId` correspond, avec sa sortie | | |
| 194 | +| `plan` | Les entrées en liste, chacune avec son état | | |
| 195 | +| `available_commands_update` | La liste que `/` ouvre dans la zone de saisie ; aussi listée, avec les descriptions, par **Agent ▸ Agent status** | | |
| 196 | +| `usage_update` | Le compte de jetons dans la barre d'état quand la fenêtre est devant | | |
| 197 | +| tout le reste | Ignoré, et compté ; le compte figure dans **Agent status** | | |
| 198 | + | |
| 199 | +Pendant qu'un tour est en cours, la règle entre les deux zones fait tourner un indicateur. Il est dessiné à partir de l'horloge et non d'un compteur, donc deux fenêtres qui réfléchissent en même temps tournent en phase et rien n'a besoin d'être remis à zéro au début d'un tour. Le *titre* de la fenêtre, lui, n'est délibérément pas animé : c'est aussi ce qu'affichent la liste des fenêtres et le menu `Alt`-chiffre, et un nom qui change huit fois par seconde les fait scintiller tous les deux. | |
| 200 | + | |
| 201 | +Une mise à jour inconnue est ignorée plutôt que refusée : le protocole grandit, et un éditeur qui cesserait de parler à un agent parce que celui-ci a appris un nouveau type de message aurait tort plus souvent que raison. | |
| 202 | + | |
| 203 | +## Coloration | |
| 204 | + | |
| 205 | +La conversation est dessinée avec des clés que tous les thèmes définissent déjà, donc aucun n'a eu besoin d'être touché : | |
| 206 | + | |
| 207 | +| Élément | Classe | | |
| 208 | +| --- | --- | | |
| 209 | +| Le nom d'un interlocuteur | `syntax.keyword` | | |
| 210 | +| Une réflexion | `syntax.comment` | | |
| 211 | +| Un appel d'outil et son état | `syntax.type` | | |
| 212 | +| Un appel d'outil en échec, et les avis de l'éditeur | `diagnostic.error` | | |
| 213 | +| Une ligne sélectionnée, et la barre du curseur | `editor.selection` | | |
| 214 | +| Le code dans un bloc délimité | l'analyseur du langage annoncé | | |
| 215 | +| Tout le reste | le texte ordinaire de la fenêtre | | |
| 216 | + | |
| 217 | +Un bloc délimité annonçant un langage que l'éditeur colore — `golo`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash` — est coloré par cet analyseur. Un bloc annonçant autre chose, ou rien, est laissé brut. | |
| 218 | + | |
| 219 | +## Tracer la conversation avec un agent | |
| 220 | + | |
| 221 | +| Variable | Effet | | |
| 222 | +| --- | --- | | |
| 223 | +| `TURBO_ACP_TRACE=<fichier>` | Ajouter à ce fichier chaque message vers et depuis chaque agent, un par ligne, horodaté et marqué `->` (envoyé) ou `<-` (reçu) | | |
| 224 | + | |
| 225 | +C'est pour la seule question à laquelle l'écran ne peut pas répondre — *qu'a réellement envoyé l'agent ?* Une mise à jour que cet éditeur ne sait pas décoder est comptée dans **Agent ▸ Agent status**, qui nomme aussi la dernière et son erreur ; la trace montre le message lui-même. Un fichier qui ne peut pas être ouvert veut dire pas de trace, et rien d'autre : la trace n'a jamais le droit de casser l'éditeur. | |
| 226 | + | |
| 227 | +## Limites | |
| 228 | + | |
| 229 | +- **Une session par fenêtre.** Fermer la fenêtre termine la session ; il n'y a pas de reprise. | |
| 230 | +- **Texte et fichiers seulement.** L'éditeur envoie du texte, et les fichiers que vous désignez par `@` ; ni images ni audio, quoi que disent les `promptCapabilities` de l'agent. | |
| 231 | +- **Pas d'authentification.** Un agent exigeant une connexion doit être connecté par sa propre CLI avant que l'éditeur ne le lance. | |
| 232 | +- **`args` n'est pas une commande shell.** `command = "sh"`, `args = ["-c", "…"]` est la façon délibérée d'en obtenir une. | |
| 233 | +- **Une entrée est plafonnée** à un mégaoctet de texte. Un agent qui déverse tout un journal de compilation ne peut pas rendre la fenêtre inutilisable ; ce qui a été perdu est signalé dans l'entrée elle-même. | |
| 234 | + | |
| 235 | +## Voir aussi | |
| 236 | + | |
| 237 | +- La tâche : [Dialoguer avec un agent de code depuis l'éditeur](../how-to/talk-to-an-agent.md) | |
| 238 | +- Le raisonnement : [Fenêtres agent](../explanation/agent-windows.md) | |
| 239 | +- Le protocole : [agentclientprotocol.com](https://agentclientprotocol.com) | |
| new file mode 100644 | |||
| @@ -0,0 +1,239 @@ | |||
| 1 | +# Agents et ACP | ||
| 2 | + | ||
| 3 | +Turbo Golo est un client de l'[Agent Client Protocol](https://agentclientprotocol.com). Il lance chaque agent comme processus fils et échange avec lui des messages JSON-RPC 2.0 sur son entrée et sa sortie standard, à raison d'un message par ligne. | ||
| 4 | + | ||
| 5 | +## Où se trouve le fichier | ||
| 6 | + | ||
| 7 | +| Chemin | Lu | Rôle | | ||
| 8 | +| --- | --- | --- | | ||
| 9 | +| `~/.config/turbo-golo/acp.toml` | en premier | Les agents que vous voulez dans tous les projets | | ||
| 10 | +| `<projet>/.turbo-gololo/acp.toml` | en second | Les agents propres à ce projet | | ||
| 11 | + | ||
| 12 | +Les deux sont facultatifs. Quand un `name` d'agent apparaît dans les deux, celui du projet remplace celui de l'utilisateur, étant l'énoncé le plus spécifique — la règle que suivent déjà les [snippets](snippets.md). Un fichier absent n'est pas une erreur ; un fichier présent mais illisible en est une, signalée sous **Agent ▸ Agent status** plutôt que de laisser silencieusement le menu vide. | ||
| 13 | + | ||
| 14 | +`TURBO_GOLO_DIR` remplace le dossier où le fichier utilisateur est cherché. Le fichier du projet est toujours `.turbo-gololo/acp.toml` sous le dossier depuis lequel l'éditeur a été lancé — il n'y a pas de remontée dans l'arborescence, pour la même raison que les [réglages de projet](project-settings.md) ne remontent pas. | ||
| 15 | + | ||
| 16 | +## Format du fichier | ||
| 17 | + | ||
| 18 | +Un bloc `[[agent]]` par agent, dans l'ordre souhaité dans le menu. | ||
| 19 | + | ||
| 20 | +```toml | ||
| 21 | +[[agent]] | ||
| 22 | +name = "Bob (llama.cpp)" | ||
| 23 | +command = "docker" | ||
| 24 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | ||
| 25 | +env = { TELEMETRY_ENABLED = "false" } | ||
| 26 | +cwd = "." | ||
| 27 | +``` | ||
| 28 | + | ||
| 29 | +| Clé | Type | Requise | Signification | | ||
| 30 | +| --- | --- | --- | --- | | ||
| 31 | +| `name` | chaîne | **oui** | Ce qu'affiche le menu Agent et le titre de la fenêtre. Doit être unique dans l'ensemble fusionné. | | ||
| 32 | +| `command` | chaîne | **oui** | L'exécutable à lancer. Cherché dans `PATH` sauf s'il contient un séparateur. | | ||
| 33 | +| `args` | liste de chaînes | non | Ses arguments, passés tels quels — pas de shell, donc ni guillemets, ni jokers, ni `&&`. | | ||
| 34 | +| `env` | table de chaînes | non | Variables d'environnement ajoutées à celles de l'éditeur. Un nom donné ici l'emporte. | | ||
| 35 | +| `cwd` | chaîne | non | Où le processus démarre, et le `cwd` annoncé à l'agent. Relatif à la racine du projet. Par défaut, la racine du projet. | | ||
| 36 | + | ||
| 37 | +`env` peut aussi s'écrire en sous-table, ce qui est la même chose : | ||
| 38 | + | ||
| 39 | +```toml | ||
| 40 | +[[agent]] | ||
| 41 | +name = "Bob (llama.cpp)" | ||
| 42 | +command = "docker" | ||
| 43 | +args = ["agent", "serve", "acp", ".turbo-gololo/agent.yaml"] | ||
| 44 | + | ||
| 45 | +[agent.env] | ||
| 46 | +TELEMETRY_ENABLED = "false" | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +### Ce qui est refusé | ||
| 50 | + | ||
| 51 | +Le fichier est refusé dans son ensemble, plutôt que chargé à moitié, dès que l'un de ces cas se présente. Un menu à moitié chargé proposant trois de vos cinq agents est pire qu'une erreur qui dit pourquoi. | ||
| 52 | + | ||
| 53 | +| Problème | Message | | ||
| 54 | +| --- | --- | | ||
| 55 | +| un agent sans `name` | `reading …/acp.toml: agent 1 has no name` | | ||
| 56 | +| un agent sans `command` | `reading …/acp.toml: agent "Bob" has no command` | | ||
| 57 | +| deux agents portant le même `name` | `reading …/acp.toml: two agents are called "Bob"` | | ||
| 58 | +| une clé que le format ne définit pas | `reading …/acp.toml: agent.comand is not a key this file has` | | ||
| 59 | + | ||
| 60 | +Le dernier cas est voulu : une clé mal orthographiée silencieusement ignorée ressemblerait exactement à une clé sans effet. | ||
| 61 | + | ||
| 62 | +## Le menu Agent | ||
| 63 | + | ||
| 64 | +`Alt-A` l'ouvre. Il est sur la barre qu'un agent soit configuré ou non, parce que c'est de là que **Create agents file** doit être atteignable. | ||
| 65 | + | ||
| 66 | +| Entrée | Active quand | Effet | | ||
| 67 | +| --- | --- | --- | | ||
| 68 | +| *une entrée par agent, par son nom* | toujours | Démarrer cet agent et ouvrir une fenêtre dessus | | ||
| 69 | +| **Create agents file** | pas d'`acp.toml` dans le projet | Écrire le fichier de départ et l'ouvrir | | ||
| 70 | +| **Cancel turn** | un tour est en cours dans la fenêtre de devant | `session/cancel` | | ||
| 71 | +| **Agent status** | toujours | Ce qui a été chargé, la ligne de commande de chacun, et ce qui a échoué | | ||
| 72 | + | ||
| 73 | +## Touches dans une fenêtre agent | ||
| 74 | + | ||
| 75 | +Une fenêtre agent est une fenêtre ordinaire : `F6`, `Alt-1`…`Alt-9`, Tile, Maximise, `[x]` et `[■]` y fonctionnent tous. À l'intérieur : | ||
| 76 | + | ||
| 77 | +| Touche | Effet | | ||
| 78 | +| --- | --- | | ||
| 79 | +| `Entrée` | Envoyer la zone de saisie comme invite | | ||
| 80 | +| `Alt-Entrée` | Insérer un saut de ligne dans la zone de saisie | | ||
| 81 | +| `Tab` | Déplacer le focus entre la conversation et la zone de saisie | | ||
| 82 | +| `Ctrl-C`, `Ctrl-Ins` | Copier la sélection, ou le bloc sur lequel est le curseur | | ||
| 83 | +| `Échap` | Abandonner la sélection ; s'il n'y en a pas, annuler le tour en cours | | ||
| 84 | +| `Ctrl-W` | Fermer la fenêtre et arrêter l'agent | | ||
| 85 | + | ||
| 86 | +Avec la **zone de saisie** au premier plan : | ||
| 87 | + | ||
| 88 | +| Touche | Effet | | ||
| 89 | +| --- | --- | | ||
| 90 | +| `↑` `↓` `←` `→` `Début` `Fin` | Déplacer le curseur dans ce que vous tapez | | ||
| 91 | +| `Retour arrière` `Suppr` | L'éditer ; le retour arrière en début de ligne la joint à celle du dessus | | ||
| 92 | +| `/` en premier caractère | Ouvrir la liste des commandes de l'agent — voir [Commandes et mentions](#commandes-et-mentions) | | ||
| 93 | +| `@` | Ouvrir la liste des fichiers du projet, réduite par ce que vous tapez ensuite | | ||
| 94 | +| `↑` `↓` `PgUp` `PgDn`, liste ouverte | Se déplacer dans la liste | | ||
| 95 | +| `Tab`, liste ouverte | Prendre l'entrée en surbrillance | | ||
| 96 | +| `Entrée`, liste ouverte | Prendre l'entrée en surbrillance ; sur un mot déjà complet, envoyer | | ||
| 97 | +| `Échap`, liste ouverte | Fermer la liste jusqu'à ce que le texte change | | ||
| 98 | + | ||
| 99 | +Avec la **conversation** au premier plan : | ||
| 100 | + | ||
| 101 | +| Touche | Effet | | ||
| 102 | +| --- | --- | | ||
| 103 | +| `↑` `↓` | Déplacer le curseur d'une ligne | | ||
| 104 | +| `PgUp` `PgDn` | Le déplacer d'un écran | | ||
| 105 | +| `Début` `Fin` | Le début de la conversation, et la fin | | ||
| 106 | +| `Shift-` l'une d'elles | Étendre la sélection à la place | | ||
| 107 | +| Glisser avec le bouton 1 | Sélectionner à la main | | ||
| 108 | +| Molette | Défiler de trois lignes sans bouger le curseur | | ||
| 109 | + | ||
| 110 | +Contrairement à une fenêtre terminal, une fenêtre agent ne **prend pas** les raccourcis de l'éditeur : il n'y a pas de shell qui ait besoin de `Ctrl-F`, donc cette touche garde son sens habituel. `Ctrl-C` fait exception, et seulement parce que rien d'autre n'en veut dans une fenêtre agent. | ||
| 111 | + | ||
| 112 | +## Commandes et mentions | ||
| 113 | + | ||
| 114 | +Deux caractères ouvrent une liste par-dessus le bas de la conversation pendant que vous tapez. Ce sont les deux mêmes que Zed, si bien que la documentation d'un agent — « tapez `/web` pour chercher » — reste vraie ici. | ||
| 115 | + | ||
| 116 | +### `/` — les commandes de l'agent | ||
| 117 | + | ||
| 118 | +Un agent peut annoncer des commandes par `available_commands_update`, au début de la session ou à tout moment pendant celle-ci. Taper `/` comme **premier caractère** de la zone de saisie les liste : le nom, la description donnée par l'agent et, entre chevrons, ce qu'il attend après le nom quand il attend quelque chose. Continuez à taper pour réduire la liste ; la correspondance porte sur le début du nom et ignore la casse. | ||
| 119 | + | ||
| 120 | +`Tab` complète la commande en surbrillance. Une commande qui prend une entrée est complétée avec une espace à la fin, pour que la suite de votre frappe soit son argument ; une qui n'en prend pas est complétée au nom seul. `Entrée` complète aussi, sauf sur un mot qui se lit déjà exactement comme une commande, où elle envoie. | ||
| 121 | + | ||
| 122 | +Sur le fil, une commande est du **texte** : `/web agent client protocol` part comme un seul bloc texte, et l'agent la reconnaît à son premier mot. C'est tout le protocole des commandes, et c'est pourquoi un `/` ailleurs qu'au début de la zone n'est qu'un caractère. | ||
| 123 | + | ||
| 124 | +Sans commande annoncée, `/` est un caractère et `Tab` garde son sens habituel. **Agent ▸ Agent status** liste les commandes avec leur description. | ||
| 125 | + | ||
| 126 | +### `@` — un fichier du projet | ||
| 127 | + | ||
| 128 | +Taper `@` n'importe où dans la zone liste les fichiers du projet, relatifs à sa racine, avec des barres obliques. Ce que vous tapez après le `@` réduit la liste : les fichiers dont le nom propre commence par cela viennent d'abord, puis ceux dont le chemin le contient seulement. `Tab` ou `Entrée` complète celui en surbrillance et ajoute une espace. | ||
| 129 | + | ||
| 130 | +À l'envoi de l'invite, chaque `@nom` qui désigne un fichier connu de la liste devient un bloc de contenu **à la place du nom** : | ||
| 131 | + | ||
| 132 | +| L'agent a déclaré | Le bloc envoyé | | ||
| 133 | +| --- | --- | | ||
| 134 | +| `promptCapabilities.embeddedContext: true` | `resource` — l'`uri` du fichier, son `mimeType` et son `text` entier, lu comme `fs/read_text_file` le lit : depuis le tampon ouvert quand le fichier est ouvert et modifié | | ||
| 135 | +| autre chose, ou le fichier n'a pas pu être lu | `resource_link` — l'`uri`, le `name` et le `mimeType`, pour que l'agent aille le chercher lui-même | | ||
| 136 | + | ||
| 137 | +Les mots de part et d'autre partent en blocs texte, si bien que `explique @docs/README.md s'il te plaît` fait trois blocs : `explique `, le fichier, ` s'il te plaît`. La conversation garde la ligne telle que vous l'avez tapée. | ||
| 138 | + | ||
| 139 | +Un mot qui commence par `@` et ne désigne aucun fichier reste du texte — une adresse électronique dans une invite n'est pas un fichier — et `@main.go` ne désigne pas `main.gopher` : le nom doit terminer le mot. | ||
| 140 | + | ||
| 141 | +La liste est le projet parcouru depuis sa racine, `.git` exclu, au plus 5 000 fichiers, et au plus 200 d'entre eux affichés à la fois. Au-delà de l'une ou l'autre limite, tapez une lettre de plus. Le parcours est refait à chaque ouverture de la liste par `@`, si bien qu'un fichier que l'agent vient de créer y figure. | ||
| 142 | + | ||
| 143 | +## La copie | ||
| 144 | + | ||
| 145 | +La sélection porte sur des **lignes entières**. Rien ne s'édite dans une conversation, donc une demi-ligne n'est jamais ce qu'on veut dire, et des lignes entières préservent l'indentation d'un bloc de code copié. | ||
| 146 | + | ||
| 147 | +Sans rien de sélectionné, la copie prend la **région sur laquelle est le curseur** : un bloc de code délimité, un passage de prose, la sortie d'un appel d'outil. Le libellé d'un interlocuteur et l'en-tête d'un appel d'outil sont du mobilier et forment des régions à part : ni l'un ni l'autre n'est jamais copié avec ce qu'il surmonte. | ||
| 148 | + | ||
| 149 | +L'indentation d'affichage de la conversation est retirée, donc le code collé arrive collé à la marge. | ||
| 150 | + | ||
| 151 | +Le texte part à deux endroits à la fois : | ||
| 152 | + | ||
| 153 | +| Presse-papiers | Comment | Collé avec | | ||
| 154 | +| --- | --- | --- | | ||
| 155 | +| Celui de l'éditeur | directement | `Shift-Ins`, dans un fichier ouvert ici | | ||
| 156 | +| Celui du système | OSC 52, à travers le terminal | `Ctrl-V`, n'importe où ailleurs | | ||
| 157 | + | ||
| 158 | +Rien ne vérifie que le terminal a accepté le second : il n'y a pas de réponse à vérifier, et un terminal peut refuser OSC 52 par sécurité ou demander qu'on l'active. Le presse-papiers de l'éditeur contient le texte dans tous les cas, et la barre d'état dit combien de lignes ont été copiées. | ||
| 159 | + | ||
| 160 | +## Quelle part du protocole est implémentée | ||
| 161 | + | ||
| 162 | +Version de protocole **1**. Turbo Golo annonce sa version dans `initialize` et accepte la version que l'agent répond, pourvu qu'il la connaisse. | ||
| 163 | + | ||
| 164 | +### Ce que l'éditeur appelle sur l'agent | ||
| 165 | + | ||
| 166 | +| Méthode | Implémentée | Remarques | | ||
| 167 | +| --- | --- | --- | | ||
| 168 | +| `initialize` | oui | Annonce la capacité `fs` ci-dessous ; `terminal` n'est pas annoncée | | ||
| 169 | +| `session/new` | oui | `cwd` vient de la clé `cwd` de l'agent ; `mcpServers` est toujours vide — les serveurs MCP sont l'affaire de l'agent | | ||
| 170 | +| `session/prompt` | oui | Des blocs texte, et un bloc `resource` ou `resource_link` par fichier désigné par `@` — voir [Commandes et mentions](#commandes-et-mentions) | | ||
| 171 | +| `session/cancel` | oui | `Échap`, et **Agent ▸ Cancel turn** | | ||
| 172 | +| `session/load` | **non** | Les conversations ne survivent pas à la fermeture de la fenêtre | | ||
| 173 | +| `authenticate` | **non** | Un agent qui liste des `authMethods` est signalé comme exigeant une connexion que l'éditeur ne sait pas faire | | ||
| 174 | + | ||
| 175 | +### Ce que l'agent peut appeler sur l'éditeur | ||
| 176 | + | ||
| 177 | +| Méthode | Implémentée | Remarques | | ||
| 178 | +| --- | --- | --- | | ||
| 179 | +| `session/update` | oui | Voir la table ci-dessous | | ||
| 180 | +| `session/request_permission` | oui | Une boîte modale portant les options de l'agent lui-même | | ||
| 181 | +| `fs/read_text_file` | oui | Depuis le tampon quand le fichier est ouvert et modifié, sinon depuis le disque | | ||
| 182 | +| `fs/write_text_file` | oui | Dans le tampon quand le fichier est ouvert, sinon sur le disque | | ||
| 183 | +| `terminal/*` | **non** | Non annoncée, donc un agent conforme ne la demandera pas | | ||
| 184 | + | ||
| 185 | +### Mises à jour de session | ||
| 186 | + | ||
| 187 | +| `sessionUpdate` | Affiché comme | | ||
| 188 | +| --- | --- | | ||
| 189 | +| `agent_message_chunk` | La réponse de l'agent, ajoutée au fil de son arrivée | | ||
| 190 | +| `agent_thought_chunk` | La même chose, dans la couleur des commentaires, sous une étiquette *réflexion* | | ||
| 191 | +| `user_message_chunk` | Votre propre message, tel que l'agent le renvoie | | ||
| 192 | +| `tool_call` | Une ligne nommant l'outil et son titre, avec son état | | ||
| 193 | +| `tool_call_update` | Repliée sur la ligne dont le `toolCallId` correspond, avec sa sortie | | ||
| 194 | +| `plan` | Les entrées en liste, chacune avec son état | | ||
| 195 | +| `available_commands_update` | La liste que `/` ouvre dans la zone de saisie ; aussi listée, avec les descriptions, par **Agent ▸ Agent status** | | ||
| 196 | +| `usage_update` | Le compte de jetons dans la barre d'état quand la fenêtre est devant | | ||
| 197 | +| tout le reste | Ignoré, et compté ; le compte figure dans **Agent status** | | ||
| 198 | + | ||
| 199 | +Pendant qu'un tour est en cours, la règle entre les deux zones fait tourner un indicateur. Il est dessiné à partir de l'horloge et non d'un compteur, donc deux fenêtres qui réfléchissent en même temps tournent en phase et rien n'a besoin d'être remis à zéro au début d'un tour. Le *titre* de la fenêtre, lui, n'est délibérément pas animé : c'est aussi ce qu'affichent la liste des fenêtres et le menu `Alt`-chiffre, et un nom qui change huit fois par seconde les fait scintiller tous les deux. | ||
| 200 | + | ||
| 201 | +Une mise à jour inconnue est ignorée plutôt que refusée : le protocole grandit, et un éditeur qui cesserait de parler à un agent parce que celui-ci a appris un nouveau type de message aurait tort plus souvent que raison. | ||
| 202 | + | ||
| 203 | +## Coloration | ||
| 204 | + | ||
| 205 | +La conversation est dessinée avec des clés que tous les thèmes définissent déjà, donc aucun n'a eu besoin d'être touché : | ||
| 206 | + | ||
| 207 | +| Élément | Classe | | ||
| 208 | +| --- | --- | | ||
| 209 | +| Le nom d'un interlocuteur | `syntax.keyword` | | ||
| 210 | +| Une réflexion | `syntax.comment` | | ||
| 211 | +| Un appel d'outil et son état | `syntax.type` | | ||
| 212 | +| Un appel d'outil en échec, et les avis de l'éditeur | `diagnostic.error` | | ||
| 213 | +| Une ligne sélectionnée, et la barre du curseur | `editor.selection` | | ||
| 214 | +| Le code dans un bloc délimité | l'analyseur du langage annoncé | | ||
| 215 | +| Tout le reste | le texte ordinaire de la fenêtre | | ||
| 216 | + | ||
| 217 | +Un bloc délimité annonçant un langage que l'éditeur colore — `golo`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash` — est coloré par cet analyseur. Un bloc annonçant autre chose, ou rien, est laissé brut. | ||
| 218 | + | ||
| 219 | +## Tracer la conversation avec un agent | ||
| 220 | + | ||
| 221 | +| Variable | Effet | | ||
| 222 | +| --- | --- | | ||
| 223 | +| `TURBO_ACP_TRACE=<fichier>` | Ajouter à ce fichier chaque message vers et depuis chaque agent, un par ligne, horodaté et marqué `->` (envoyé) ou `<-` (reçu) | | ||
| 224 | + | ||
| 225 | +C'est pour la seule question à laquelle l'écran ne peut pas répondre — *qu'a réellement envoyé l'agent ?* Une mise à jour que cet éditeur ne sait pas décoder est comptée dans **Agent ▸ Agent status**, qui nomme aussi la dernière et son erreur ; la trace montre le message lui-même. Un fichier qui ne peut pas être ouvert veut dire pas de trace, et rien d'autre : la trace n'a jamais le droit de casser l'éditeur. | ||
| 226 | + | ||
| 227 | +## Limites | ||
| 228 | + | ||
| 229 | +- **Une session par fenêtre.** Fermer la fenêtre termine la session ; il n'y a pas de reprise. | ||
| 230 | +- **Texte et fichiers seulement.** L'éditeur envoie du texte, et les fichiers que vous désignez par `@` ; ni images ni audio, quoi que disent les `promptCapabilities` de l'agent. | ||
| 231 | +- **Pas d'authentification.** Un agent exigeant une connexion doit être connecté par sa propre CLI avant que l'éditeur ne le lance. | ||
| 232 | +- **`args` n'est pas une commande shell.** `command = "sh"`, `args = ["-c", "…"]` est la façon délibérée d'en obtenir une. | ||
| 233 | +- **Une entrée est plafonnée** à un mégaoctet de texte. Un agent qui déverse tout un journal de compilation ne peut pas rendre la fenêtre inutilisable ; ce qui a été perdu est signalé dans l'entrée elle-même. | ||
| 234 | + | ||
| 235 | +## Voir aussi | ||
| 236 | + | ||
| 237 | +- La tâche : [Dialoguer avec un agent de code depuis l'éditeur](../how-to/talk-to-an-agent.md) | ||
| 238 | +- Le raisonnement : [Fenêtres agent](../explanation/agent-windows.md) | ||
| 239 | +- Le protocole : [agentclientprotocol.com](https://agentclientprotocol.com) | ||
added
docs/fr/reference/cli.md +110 -0 | new file mode 100644 | ||
| @@ -0,0 +1,110 @@ | ||
| 1 | +# Référence : ligne de commande | |
| 2 | + | |
| 3 | +> Description neutre de la commande `turbo-golo`, de ses options et de l'environnement qu'elle lit. | |
| 4 | + | |
| 5 | +## Synopsis | |
| 6 | + | |
| 7 | +``` | |
| 8 | +turbo-golo [options] [fichier...] | |
| 9 | +``` | |
| 10 | + | |
| 11 | +Chaque `fichier` est ouvert dans sa propre fenêtre. Un fichier qui n'existe pas encore est ouvert comme un tampon vide associé à ce chemin. Sans aucun fichier, une seule fenêtre vide sans titre est ouverte. | |
| 12 | + | |
| 13 | +## Options | |
| 14 | + | |
| 15 | +| Option | Type | Défaut | Description | | |
| 16 | +| --- | --- | --- | --- | | |
| 17 | +| `-theme <nom>` | chaîne | celui du projet, sinon `turbo-classic` | Thème de démarrage, l'emportant sur celui du projet. Un nom inconnu retombe sur le thème par défaut sans erreur. | | |
| 18 | +| `-list-themes` | booléen | `false` | Affiche chaque thème chargeable avec sa description, puis le répertoire de thèmes utilisateur, et quitte. | | |
| 19 | +| `-no-lsp` | booléen | `false` | Ne démarre pas de serveur de langage. La coloration et l'édition sont inchangées. | | |
| 20 | +| `-version` | booléen | `false` | Affiche `Turbo Golo <version>` sur une ligne, avec le commit et la date de build quand le build les a enregistrés, puis quitte. Voir [le numéro de version](versioning.md). | | |
| 21 | +| `-h`, `-help` | booléen | `false` | Affiche la liste des options et quitte. | | |
| 22 | + | |
| 23 | +## Environnement | |
| 24 | + | |
| 25 | +| Variable | Lue par | Effet | | |
| 26 | +| --- | --- | --- | | |
| 27 | +| `TURBO_GOLO_DIR` | répertoire utilisateur | Remplace `~/.config/turbo-golo` : les thèmes sont alors lus dans `$TURBO_GOLO_DIR/themes`, et vos snippets dans `$TURBO_GOLO_DIR/snippets.toml`. | | |
| 28 | +| `TURBO_GOLO_THEME_DIR` | chargement des thèmes | Répertoire des thèmes utilisateur, à la place de `<répertoire utilisateur>/themes`. | | |
| 29 | +| `TURBO_GOLO_SNIPPET_DIR` | chargement des snippets | Répertoire contenant votre `snippets.toml`, à la place du répertoire utilisateur. | | |
| 30 | +| `PATH` | recherche de `golo` | Consulté en premier ; `/usr/local/bin` est ensuite regardé même s'il n'y figure pas. Aucune variable ne nomme un autre répertoire. | | |
| 31 | +| `TERM` | tcell | Description de terminal à utiliser. | | |
| 32 | +| `SHELL` | fenêtres terminal | Le shell qu'ouvre `F8` ; `/bin/sh` si elle est absente. | | |
| 33 | + | |
| 34 | +## Fichiers | |
| 35 | + | |
| 36 | +| Chemin | Rôle | | |
| 37 | +| --- | --- | | |
| 38 | +| `./.turbo-golo/settings.toml` | Les réglages de ce projet, lus au démarrage puis à chaque enregistrement. Voir [réglages de projet](project-settings.md). | | |
| 39 | +| `./.turbo-golo/snippets.toml` | Les snippets de ce projet. Voir [snippets](snippets.md). | | |
| 40 | +| `./.turbo-golo/tools.toml` | Les outils du menu Golo. Voir [outils Golo](golo-tools.md). | | |
| 41 | +| `~/.config/turbo-golo/themes/*.toml` | Thèmes utilisateur sous Linux (`os.UserConfigDir`). | | |
| 42 | +| `~/Library/Application Support/turbo-golo/themes/*.toml` | Thèmes utilisateur sous macOS. | | |
| 43 | +| `~/.config/turbo-golo/snippets.toml` | Vos snippets, partagés entre projets. | | |
| 44 | +| `$TURBO_GOLO_THEME_DIR/*.toml` | Thèmes utilisateur, quand la variable est définie. | | |
| 45 | +| `/usr/local/bin/golo` | Le serveur de langage, quand `golo` n'est pas dans le `PATH`. | | |
| 46 | + | |
| 47 | +Le serveur de langage reçoit comme racine le dossier du premier fichier nommé sur la ligne de commande, ou le répertoire de travail quand aucun ne l'est. Aucun fichier marqueur n'est cherché en remontant : Golo n'a pas de manifeste de projet. | |
| 48 | + | |
| 49 | +## Code de sortie | |
| 50 | + | |
| 51 | +| Code | Signification | | |
| 52 | +| --- | --- | | |
| 53 | +| `0` | L'éditeur s'est terminé normalement, ou une option d'information a été utilisée. | | |
| 54 | +| `1` | Le terminal n'a pas pu être ouvert ou initialisé. La raison est écrite sur la sortie d'erreur. | | |
| 55 | + | |
| 56 | +## Cibles make | |
| 57 | + | |
| 58 | +À exécuter depuis un clone. | |
| 59 | + | |
| 60 | +| Cible | Ce qu'elle lance | | |
| 61 | +| --- | --- | | |
| 62 | +| `make help` | Liste les cibles. C'est la cible par défaut. | | |
| 63 | +| `make test` | `go test ./...` | | |
| 64 | +| `make test-verbose` | `go test -v ./...` | | |
| 65 | +| `make cover` | `go test -cover ./...` | | |
| 66 | +| `make build` | `go build` estampillé dans `bin/turbo-golo`, puis `scripts/check-version.sh` sur le résultat | | |
| 67 | +| `make version` | Affiche la version que ce clone estampillerait, sans construire | | |
| 68 | +| `make ldflags` | Affiche les options d'édition de liens d'un build estampillé | | |
| 69 | +| `make install` | `scripts/install.sh` — compile et installe dans le PATH | | |
| 70 | +| `make uninstall` | `scripts/install.sh --uninstall` | | |
| 71 | +| `make run FILE=x.golo` | `make build`, puis `./bin/turbo-golo x.golo` | | |
| 72 | +| `make fmt` | `go fmt ./...` | | |
| 73 | +| `make vet` | `go vet ./...` | | |
| 74 | +| `make check` | `fmt`, puis `vet`, puis `test` | | |
| 75 | +| `make clean` | Supprime `bin/` | | |
| 76 | + | |
| 77 | +## Exemples | |
| 78 | + | |
| 79 | +```bash | |
| 80 | +turbo-golo # une fenêtre vide | |
| 81 | +turbo-golo main.golo README.md # deux fenêtres | |
| 82 | +turbo-golo -theme turbo-dark main.golo # un autre thème | |
| 83 | +turbo-golo -no-lsp main.golo # sans serveur de langage | |
| 84 | +turbo-golo -list-themes # quels thèmes existent | |
| 85 | +``` | |
| 86 | + | |
| 87 | +## Installateur | |
| 88 | + | |
| 89 | +`scripts/install.sh`, également accessible via `make install`. | |
| 90 | + | |
| 91 | +| Option | Description | | |
| 92 | +| --- | --- | | |
| 93 | +| `-p`, `--prefix RÉP` | Installer dans `RÉP` au lieu de `$GOBIN` ou `$GOPATH/bin`. | | |
| 94 | +| `--with-server` | Construire et installer GoloScript — `golo`, `gogolo` et `wagolo` — depuis ses sources, s'il n'est pas déjà présent. | | |
| 95 | +| `--uninstall` | Retirer un `turbo-golo` installé, puis s'arrêter. | | |
| 96 | +| `-h`, `--help` | Afficher les options, puis s'arrêter. | | |
| 97 | + | |
| 98 | +| Code de sortie | Signification | | |
| 99 | +| --- | --- | | |
| 100 | +| `0` | Installé, retiré, ou aide affichée. | | |
| 101 | +| `1` | Go absent ou trop ancien, échec de compilation, ou destination non accessible en écriture. Rien n'est installé et une installation existante reste intacte. | | |
| 102 | + | |
| 103 | +## Erreurs | |
| 104 | + | |
| 105 | +| Message | Cause | | |
| 106 | +| --- | --- | | |
| 107 | +| `turbo-golo: opening the terminal: …` | tcell n'a pas pu ouvrir le terminal ; en général `TERM` est absent ou inconnu. | | |
| 108 | +| `turbo-golo: initialising the terminal: …` | Le terminal a été ouvert mais n'a pas pu être mis en mode brut. | | |
| 109 | +| `Cannot open` (dans une boîte) | Le chemin est un répertoire, ou n'est pas lisible. | | |
| 110 | +| `Cannot save` (dans une boîte) | Le répertoire n'existe pas, ou n'est pas accessible en écriture. | | |
| new file mode 100644 | |||
| @@ -0,0 +1,110 @@ | |||
| 1 | +# Référence : ligne de commande | ||
| 2 | + | ||
| 3 | +> Description neutre de la commande `turbo-golo`, de ses options et de l'environnement qu'elle lit. | ||
| 4 | + | ||
| 5 | +## Synopsis | ||
| 6 | + | ||
| 7 | +``` | ||
| 8 | +turbo-golo [options] [fichier...] | ||
| 9 | +``` | ||
| 10 | + | ||
| 11 | +Chaque `fichier` est ouvert dans sa propre fenêtre. Un fichier qui n'existe pas encore est ouvert comme un tampon vide associé à ce chemin. Sans aucun fichier, une seule fenêtre vide sans titre est ouverte. | ||
| 12 | + | ||
| 13 | +## Options | ||
| 14 | + | ||
| 15 | +| Option | Type | Défaut | Description | | ||
| 16 | +| --- | --- | --- | --- | | ||
| 17 | +| `-theme <nom>` | chaîne | celui du projet, sinon `turbo-classic` | Thème de démarrage, l'emportant sur celui du projet. Un nom inconnu retombe sur le thème par défaut sans erreur. | | ||
| 18 | +| `-list-themes` | booléen | `false` | Affiche chaque thème chargeable avec sa description, puis le répertoire de thèmes utilisateur, et quitte. | | ||
| 19 | +| `-no-lsp` | booléen | `false` | Ne démarre pas de serveur de langage. La coloration et l'édition sont inchangées. | | ||
| 20 | +| `-version` | booléen | `false` | Affiche `Turbo Golo <version>` sur une ligne, avec le commit et la date de build quand le build les a enregistrés, puis quitte. Voir [le numéro de version](versioning.md). | | ||
| 21 | +| `-h`, `-help` | booléen | `false` | Affiche la liste des options et quitte. | | ||
| 22 | + | ||
| 23 | +## Environnement | ||
| 24 | + | ||
| 25 | +| Variable | Lue par | Effet | | ||
| 26 | +| --- | --- | --- | | ||
| 27 | +| `TURBO_GOLO_DIR` | répertoire utilisateur | Remplace `~/.config/turbo-golo` : les thèmes sont alors lus dans `$TURBO_GOLO_DIR/themes`, et vos snippets dans `$TURBO_GOLO_DIR/snippets.toml`. | | ||
| 28 | +| `TURBO_GOLO_THEME_DIR` | chargement des thèmes | Répertoire des thèmes utilisateur, à la place de `<répertoire utilisateur>/themes`. | | ||
| 29 | +| `TURBO_GOLO_SNIPPET_DIR` | chargement des snippets | Répertoire contenant votre `snippets.toml`, à la place du répertoire utilisateur. | | ||
| 30 | +| `PATH` | recherche de `golo` | Consulté en premier ; `/usr/local/bin` est ensuite regardé même s'il n'y figure pas. Aucune variable ne nomme un autre répertoire. | | ||
| 31 | +| `TERM` | tcell | Description de terminal à utiliser. | | ||
| 32 | +| `SHELL` | fenêtres terminal | Le shell qu'ouvre `F8` ; `/bin/sh` si elle est absente. | | ||
| 33 | + | ||
| 34 | +## Fichiers | ||
| 35 | + | ||
| 36 | +| Chemin | Rôle | | ||
| 37 | +| --- | --- | | ||
| 38 | +| `./.turbo-golo/settings.toml` | Les réglages de ce projet, lus au démarrage puis à chaque enregistrement. Voir [réglages de projet](project-settings.md). | | ||
| 39 | +| `./.turbo-golo/snippets.toml` | Les snippets de ce projet. Voir [snippets](snippets.md). | | ||
| 40 | +| `./.turbo-golo/tools.toml` | Les outils du menu Golo. Voir [outils Golo](golo-tools.md). | | ||
| 41 | +| `~/.config/turbo-golo/themes/*.toml` | Thèmes utilisateur sous Linux (`os.UserConfigDir`). | | ||
| 42 | +| `~/Library/Application Support/turbo-golo/themes/*.toml` | Thèmes utilisateur sous macOS. | | ||
| 43 | +| `~/.config/turbo-golo/snippets.toml` | Vos snippets, partagés entre projets. | | ||
| 44 | +| `$TURBO_GOLO_THEME_DIR/*.toml` | Thèmes utilisateur, quand la variable est définie. | | ||
| 45 | +| `/usr/local/bin/golo` | Le serveur de langage, quand `golo` n'est pas dans le `PATH`. | | ||
| 46 | + | ||
| 47 | +Le serveur de langage reçoit comme racine le dossier du premier fichier nommé sur la ligne de commande, ou le répertoire de travail quand aucun ne l'est. Aucun fichier marqueur n'est cherché en remontant : Golo n'a pas de manifeste de projet. | ||
| 48 | + | ||
| 49 | +## Code de sortie | ||
| 50 | + | ||
| 51 | +| Code | Signification | | ||
| 52 | +| --- | --- | | ||
| 53 | +| `0` | L'éditeur s'est terminé normalement, ou une option d'information a été utilisée. | | ||
| 54 | +| `1` | Le terminal n'a pas pu être ouvert ou initialisé. La raison est écrite sur la sortie d'erreur. | | ||
| 55 | + | ||
| 56 | +## Cibles make | ||
| 57 | + | ||
| 58 | +À exécuter depuis un clone. | ||
| 59 | + | ||
| 60 | +| Cible | Ce qu'elle lance | | ||
| 61 | +| --- | --- | | ||
| 62 | +| `make help` | Liste les cibles. C'est la cible par défaut. | | ||
| 63 | +| `make test` | `go test ./...` | | ||
| 64 | +| `make test-verbose` | `go test -v ./...` | | ||
| 65 | +| `make cover` | `go test -cover ./...` | | ||
| 66 | +| `make build` | `go build` estampillé dans `bin/turbo-golo`, puis `scripts/check-version.sh` sur le résultat | | ||
| 67 | +| `make version` | Affiche la version que ce clone estampillerait, sans construire | | ||
| 68 | +| `make ldflags` | Affiche les options d'édition de liens d'un build estampillé | | ||
| 69 | +| `make install` | `scripts/install.sh` — compile et installe dans le PATH | | ||
| 70 | +| `make uninstall` | `scripts/install.sh --uninstall` | | ||
| 71 | +| `make run FILE=x.golo` | `make build`, puis `./bin/turbo-golo x.golo` | | ||
| 72 | +| `make fmt` | `go fmt ./...` | | ||
| 73 | +| `make vet` | `go vet ./...` | | ||
| 74 | +| `make check` | `fmt`, puis `vet`, puis `test` | | ||
| 75 | +| `make clean` | Supprime `bin/` | | ||
| 76 | + | ||
| 77 | +## Exemples | ||
| 78 | + | ||
| 79 | +```bash | ||
| 80 | +turbo-golo # une fenêtre vide | ||
| 81 | +turbo-golo main.golo README.md # deux fenêtres | ||
| 82 | +turbo-golo -theme turbo-dark main.golo # un autre thème | ||
| 83 | +turbo-golo -no-lsp main.golo # sans serveur de langage | ||
| 84 | +turbo-golo -list-themes # quels thèmes existent | ||
| 85 | +``` | ||
| 86 | + | ||
| 87 | +## Installateur | ||
| 88 | + | ||
| 89 | +`scripts/install.sh`, également accessible via `make install`. | ||
| 90 | + | ||
| 91 | +| Option | Description | | ||
| 92 | +| --- | --- | | ||
| 93 | +| `-p`, `--prefix RÉP` | Installer dans `RÉP` au lieu de `$GOBIN` ou `$GOPATH/bin`. | | ||
| 94 | +| `--with-server` | Construire et installer GoloScript — `golo`, `gogolo` et `wagolo` — depuis ses sources, s'il n'est pas déjà présent. | | ||
| 95 | +| `--uninstall` | Retirer un `turbo-golo` installé, puis s'arrêter. | | ||
| 96 | +| `-h`, `--help` | Afficher les options, puis s'arrêter. | | ||
| 97 | + | ||
| 98 | +| Code de sortie | Signification | | ||
| 99 | +| --- | --- | | ||
| 100 | +| `0` | Installé, retiré, ou aide affichée. | | ||
| 101 | +| `1` | Go absent ou trop ancien, échec de compilation, ou destination non accessible en écriture. Rien n'est installé et une installation existante reste intacte. | | ||
| 102 | + | ||
| 103 | +## Erreurs | ||
| 104 | + | ||
| 105 | +| Message | Cause | | ||
| 106 | +| --- | --- | | ||
| 107 | +| `turbo-golo: opening the terminal: …` | tcell n'a pas pu ouvrir le terminal ; en général `TERM` est absent ou inconnu. | | ||
| 108 | +| `turbo-golo: initialising the terminal: …` | Le terminal a été ouvert mais n'a pas pu être mis en mode brut. | | ||
| 109 | +| `Cannot open` (dans une boîte) | Le chemin est un répertoire, ou n'est pas lisible. | | ||
| 110 | +| `Cannot save` (dans une boîte) | Le répertoire n'existe pas, ou n'est pas accessible en écriture. | | ||
added
docs/fr/reference/golo-tools.md +244 -0 | new file mode 100644 | ||
| @@ -0,0 +1,244 @@ | ||
| 1 | +# Référence : outils Golo | |
| 2 | + | |
| 3 | +> Description neutre de `.turbo-golo/tools.toml`, du menu Golo, et de ce que fait l'exécution d'une commande. | |
| 4 | + | |
| 5 | +## Fichier | |
| 6 | + | |
| 7 | +| Propriété | Valeur | | |
| 8 | +| --- | --- | | |
| 9 | +| Chemin | `./.turbo-golo/tools.toml` | | |
| 10 | +| Recherche | Le répertoire courant seulement. Les répertoires parents ne sont **pas** parcourus. | | |
| 11 | +| Lecture | À chaque ouverture de l'un de ses menus, pour les entrées | | |
| 12 | +| Relecture | Dès que la taille ou la date de modification du fichier change, pour l'**ensemble** des menus | | |
| 13 | +| Fichier absent | Pas une erreur | | |
| 14 | +| Fichier illisible | Une erreur, affichée dans le menu | | |
| 15 | +| Fichier au niveau utilisateur | **Aucun.** Contrairement aux snippets, il n'y a pas de `~/.config/turbo-golo/tools.toml`. | | |
| 16 | + | |
| 17 | +## Format du fichier | |
| 18 | + | |
| 19 | +Une table `[[tool]]` par commande. | |
| 20 | + | |
| 21 | +| Clé | Type | Obligatoire | Description | | |
| 22 | +| --- | --- | --- | --- | | |
| 23 | +| `name` | chaîne | oui | Ce que le menu affiche. Peut porter une touche chaude entre tildes, comme `"~T~est"`. | | |
| 24 | +| `command` | chaîne | oui | La commande shell à lancer | | |
| 25 | +| `output` | chaîne | non | Où va sa sortie : `popup`, `terminal` ou `editor`. Absent signifie `popup`. | | |
| 26 | +| `menu` | chaîne | non | Dans quel menu elle apparaît. Absent signifie `Golo`. N'importe quel nom ; le menu est créé pour vous. Peut porter une touche chaude entre tildes. | | |
| 27 | + | |
| 28 | +`menu` n'est pas vérifié contre une liste, parce qu'il n'y a pas de liste : un nom qu'aucun autre outil n'utilise crée simplement un menu. Un outil sans `name`, sans `command`, ou dont l'`output` nomme quelque chose qui n'existe pas rend tout le fichier erroné. Un `output` inconnu est **refusé plutôt que corrigé** : `"termnial"` aurait sinon l'air d'avoir marché tout en envoyant la sortie ailleurs. | |
| 29 | + | |
| 30 | +### Exemple | |
| 31 | + | |
| 32 | +```toml | |
| 33 | +[[tool]] | |
| 34 | +name = "~T~est" | |
| 35 | +command = "golo --test" | |
| 36 | +output = "popup" | |
| 37 | + | |
| 38 | +[[tool]] | |
| 39 | +name = "~E~cho" | |
| 40 | +command = "echo TADA" | |
| 41 | +output = "terminal" | |
| 42 | +menu = "Tools" | |
| 43 | +``` | |
| 44 | + | |
| 45 | +## Le fichier de départ | |
| 46 | + | |
| 47 | +**Golo ▸ Create tools file** écrit ces neuf outils, dans cet ordre : | |
| 48 | + | |
| 49 | +| Nom | Commande | Sortie | Menu | | |
| 50 | +| --- | --- | --- | --- | | |
| 51 | +| `~R~un` | `golo {{script, e.g. main.golo}}` | `terminal` | Golo | | |
| 52 | +| `~T~est` | `golo --test` | `popup` | Golo | | |
| 53 | +| `Test ~o~ne` | `golo --test {{test file or directory}}` | `popup` | Golo | | |
| 54 | +| `~D~ebug` | `golo --debug {{script, e.g. main.golo}}` | `terminal` | Golo | | |
| 55 | +| `R~E~PL` | `golo` | `terminal` | Golo | | |
| 56 | +| `~N~ew script` | `golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}` | `popup` | Golo | | |
| 57 | +| `~B~uild native` | `gogolo build -o {{output executable}} {{script, e.g. main.golo}}` | `popup` | Golo | | |
| 58 | +| `Build ~w~asm` | `wagolo build -target={{target: wasi, js or wasip2}} -o {{output .wasm}} {{script, e.g. main.golo}}` | `popup` | Golo | | |
| 59 | +| `~E~cho` | `echo 🎉 tada!` | `terminal` | Tools | | |
| 60 | + | |
| 61 | +`Run` vient en premier parce que Golo est un langage de script et qu'exécuter le fichier est ce qu'un programmeur Golo fait le plus. Six d'entre eux demandent une valeur avant de s'exécuter — Golo n'a pas de manifeste, chaque commande qui touche un fichier doit donc se faire dire lequel — et un nomme un `menu` à lui. Ces deux fonctionnalités sont invisibles si le fichier de départ ne les montre pas. | |
| 62 | + | |
| 63 | +`Run`, `Debug` et `REPL` reçoivent un terminal : les deux premiers peuvent lire le clavier, et le troisième n'est rien d'autre. `Build native` a besoin de l'outillage Go sur le `PATH` ; `Build wasm` a besoin de TinyGo, et de `wasm-tools` pour la cible `wasip2`. | |
| 64 | + | |
| 65 | +Chaque outil nomme son `output`, y compris ceux qui nomment le défaut : la clé est la partie intéressante du format, et un fichier où elle apparaît une fois est un fichier où personne ne remarque qu'elle existe. | |
| 66 | + | |
| 67 | +L'entrée est grisée une fois que le projet a un fichier d'outils, elle ne peut donc pas en écraser un. Le fichier est écrit via un fichier temporaire dans le même répertoire, renommé en place. | |
| 68 | + | |
| 69 | +## Le menu Golo | |
| 70 | + | |
| 71 | +Toujours sur la barre, qu'un fichier d'outils existe ou non. Sa touche chaude est `Alt-G`. | |
| 72 | + | |
| 73 | +| Entrée | Condition | | |
| 74 | +| --- | --- | | |
| 75 | +| Une ligne par outil sans `menu`, dans l'ordre du fichier | Le fichier en contient au moins un | | |
| 76 | +| `Cannot read tools`, grisé | Le fichier est présent mais illisible | | |
| 77 | +| `Create tools file` | Le projet n'a pas de fichier d'outils | | |
| 78 | +| `Open tools file` | Le projet en a un | | |
| 79 | + | |
| 80 | +## Les menus qu'un outil demande | |
| 81 | + | |
| 82 | +Un `menu` nommant autre chose que `Golo` met un menu de ce nom sur la barre. | |
| 83 | + | |
| 84 | +| Propriété | Valeur | | |
| 85 | +| --- | --- | | |
| 86 | +| Position | Entre Golo et Help | | |
| 87 | +| Ordre | L'ordre dans lequel chaque nom apparaît pour la première fois dans le fichier | | |
| 88 | +| Entrées | Une ligne par outil nommant ce menu, dans l'ordre du fichier. Rien d'autre — `Create tools file` et `Open tools file` restent dans Golo. | | |
| 89 | +| Fichier illisible | Aucun menu du tout ; le menu Golo porte l'erreur | | |
| 90 | +| Pendant que l'éditeur tourne | Ajoutés, retirés et renommés au fil des changements du fichier, sans redémarrage | | |
| 91 | + | |
| 92 | +### Touches chaudes | |
| 93 | + | |
| 94 | +Attribuées automatiquement, parce qu'un nom venu d'un fichier ne peut pas être vérifié à l'avance contre les menus fixes. | |
| 95 | + | |
| 96 | +| Cas | Résultat | | |
| 97 | +| --- | --- | | |
| 98 | +| Pas de tilde dans le nom | La première lettre qu'aucun autre menu n'a revendiquée est marquée. `Format` devient `For~m~at` : `F` est à File, `o` à Options, `r` à Run. | | |
| 99 | +| Des tildes nommant une lettre libre | Conservés tels quels. `Doc~k~er` répond à `Alt-K`. | | |
| 100 | +| Des tildes nommant une lettre prise | Abandonnés, et une lettre libre choisie à la place. `~F~oo` devient `F~o~o`. | | |
| 101 | +| Toutes les lettres prises | Pas de touche chaude. `F10` et la souris l'ouvrent toujours. | | |
| 102 | + | |
| 103 | +Les lettres que tiennent les menus propres de l'éditeur sont `F`, `E`, `S`, `R`, `C`, `O`, `W`, `N` (Snippets), `G` (Golo) et `H`. | |
| 104 | + | |
| 105 | +## Exécuter une commande | |
| 106 | + | |
| 107 | +Commun à toutes les sorties : | |
| 108 | + | |
| 109 | +| Propriété | Valeur | | |
| 110 | +| --- | --- | | |
| 111 | +| Shell | `/bin/sh -c "<commande>"` sous Linux et macOS ; `cmd.exe /S /C "<commande>"` — le shell que nomme `%COMSPEC%` — sous Windows | | |
| 112 | +| Répertoire | Le répertoire dans lequel l'éditeur a été lancé | | |
| 113 | +| Sortie d'erreur | Fusionnée dans la sortie standard, dans l'ordre où la commande les a écrites | | |
| 114 | + | |
| 115 | +Passer par un shell signifie que les tubes, les globs, `&&` et `;` marchent tous, un outil peut donc être une séquence. Sous Windows le shell est cmd.exe, qui connaît `&&`, `|` et `>` mais ne développe pas les globs, et où `;` n'est pas un séparateur. | |
| 116 | + | |
| 117 | +### `output = "popup"` | |
| 118 | + | |
| 119 | +| Propriété | Valeur | | |
| 120 | +| --- | --- | | |
| 121 | +| S'ouvre | Immédiatement, avant que la commande soit finie | | |
| 122 | +| Modal | Oui : rien d'autre dans l'éditeur n'est utilisable pendant qu'il est là | | |
| 123 | +| Se remplit | À mesure que la sortie arrive, en la suivant jusqu'à ce que vous remontiez | | |
| 124 | +| Titre pendant l'exécution | `<commande> — running` | | |
| 125 | +| Titre une fois fini | `<commande> — ok`, ou `<commande> — exit <n>` | | |
| 126 | +| Sortie vide, fini | Affiche `(no output)` | | |
| 127 | +| Sortie vide, en cours | N'affiche rien | | |
| 128 | +| Plafond de sortie | 10000 lignes ; au-delà les plus anciennes partent et une ligne `… n earlier lines dropped …` le dit | | |
| 129 | + | |
| 130 | +| Touche | Effet | | |
| 131 | +| --- | --- | | |
| 132 | +| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Parcourir la sortie | | |
| 133 | +| Molette | Idem | | |
| 134 | +| `Échap`, `Entrée`, **Close** | Le fermer, **en arrêtant la commande** si elle tourne encore | | |
| 135 | + | |
| 136 | +Fermer arrête la commande parce qu'il n'y a pas d'autre moyen d'interrompre une commande dont la sortie n'est pas dans un terminal. | |
| 137 | + | |
| 138 | +### `output = "terminal"` | |
| 139 | + | |
| 140 | +| Propriété | Valeur | | |
| 141 | +| --- | --- | | |
| 142 | +| Fenêtre | Une fenêtre de terminal à elle, titrée de la commande | | |
| 143 | +| Environnement | Celui de l'éditeur, avec `TERM` à `xterm-256color` | | |
| 144 | +| Après sa fin | La fenêtre reste, montrant sa sortie | | |
| 145 | +| Modal | Non : l'éditeur continue à côté | | |
| 146 | + | |
| 147 | +Comme c'est un vrai terminal, les couleurs, la pagination, `Ctrl-C` et la lecture du clavier marchent tous — les coches vertes de `golo --test`, `readln` dans un script, l'invite du REPL. Voir [Fenêtres de terminal](terminal.md). | |
| 148 | + | |
| 149 | +Touches dans une fenêtre de terminal **terminée** : | |
| 150 | + | |
| 151 | +| Touche | Effet | | |
| 152 | +| --- | --- | | |
| 153 | +| `Shift-PgUp`, `Shift-PgDn` | Remonter dans la sortie | | |
| 154 | +| `Ctrl-W` | Fermer la fenêtre | | |
| 155 | +| Tout le reste | Atteint l'éditeur, pas le shell mort | | |
| 156 | + | |
| 157 | +### `output = "editor"` | |
| 158 | + | |
| 159 | +| Propriété | Valeur | | |
| 160 | +| --- | --- | | |
| 161 | +| Affiche | Un popup pendant l'exécution, comme ci-dessus | | |
| 162 | +| À la fermeture du popup | Une fenêtre d'édition contenant la sortie, titrée de la commande | | |
| 163 | +| Remplie | Une fois, quand la commande est finie — pas au fil de l'eau | | |
| 164 | +| La fenêtre | Une fenêtre d'édition ordinaire sans nom de fichier : `Ctrl-F` la fouille, et `Save as` la conserve | | |
| 165 | + | |
| 166 | +## Rechargement après une commande | |
| 167 | + | |
| 168 | +Quand une commande se termine, chaque fichier ouvert est considéré. | |
| 169 | + | |
| 170 | +| Le fichier | Ce qui se passe | | |
| 171 | +| --- | --- | | |
| 172 | +| Non modifié, et changé sur le disque | Relu ; sa syntaxe est redécidée et son titre rafraîchi | | |
| 173 | +| Non modifié, et inchangé sur le disque | Laissé tranquille, non compté | | |
| 174 | +| Avec des modifications non enregistrées | Laissé tranquille et compté comme ignoré | | |
| 175 | +| Jamais nommé | Laissé tranquille | | |
| 176 | +| Disparu du disque | Laissé tranquille | | |
| 177 | + | |
| 178 | +Le curseur reste où il était, ramené dans ce que le fichier contient désormais. L'historique d'annulation est jeté, parce qu'annuler au-delà d'un rechargement restaurerait un texte que le fichier n'a plus. | |
| 179 | + | |
| 180 | +L'arbre du projet est rafraîchi au même instant — c'est ainsi qu'un fichier écrit par `golo new` y apparaît. | |
| 181 | + | |
| 182 | +| Barre d'état | Quand | | |
| 183 | +| --- | --- | | |
| 184 | +| `Running <commande>` | La fenêtre s'ouvre | | |
| 185 | +| `Reloaded 2 files` | Deux fichiers ont été relus, aucun ignoré | | |
| 186 | +| `Reloaded 2 files; 1 file with unsaved changes left alone` | Certains ont été ignorés | | |
| 187 | +| `Command finished; 1 file with unsaved changes left alone` | Rien n'a été relu, quelque chose a été ignoré | | |
| 188 | + | |
| 189 | +## Erreurs | |
| 190 | + | |
| 191 | +| Message | Cause | | |
| 192 | +| --- | --- | | |
| 193 | +| `Cannot read tools` dans le menu | Le fichier est présent mais n'est pas du TOML valide, ou contient un outil sans nom ou sans commande | | |
| 194 | +| `Already there: .turbo-golo/tools.toml` | Création dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; toujours possible pour un appelant qui n'est pas un menu. | | |
| 195 | +| `This project has no .turbo-golo/tools.toml yet.` | Ouverture dans un projet qui n'en a pas, de même | | |
| 196 | +| `Cannot tell which directory this is: …` | Le répertoire courant n'a pas pu être lu | | |
| 197 | +| `Terminal windows are not supported on this platform yet` | Lancer une commande demande un pseudo-terminal, que Linux, macOS et Windows possèdent ; voir [Fenêtres de terminal](terminal.md) | | |
| 198 | + | |
| 199 | +## Demander une valeur | |
| 200 | + | |
| 201 | +Un `{{libellé}}` n'importe où dans une commande est une valeur que l'éditeur demande avant de l'exécuter, dans une boîte titrée du nom de l'outil. Le texte entre les accolades est ce que la boîte demande. | |
| 202 | + | |
| 203 | +| Écrit | Demandé | Substitué | | |
| 204 | +| --- | --- | --- | | |
| 205 | +| `{{script, e.g. main.golo}}` | `script, e.g. main.golo` | cité pour le shell | | |
| 206 | +| `{{arguments...}}` | `arguments` | tel quel | | |
| 207 | + | |
| 208 | +Une valeur est **citée pour le shell** par défaut, un chemin avec un espace reste donc un seul argument. Un `...` final dans les accolades la demande telle quelle, et c'est ainsi qu'un champ peut valoir plusieurs arguments. | |
| 209 | + | |
| 210 | +```toml | |
| 211 | +[[tool]] | |
| 212 | +name = "Run with ~a~rguments" | |
| 213 | +command = "golo main.golo {{arguments...}}" | |
| 214 | +output = "terminal" | |
| 215 | +``` | |
| 216 | + | |
| 217 | +| Règle | Comportement | | |
| 218 | +| --- | --- | | |
| 219 | +| Plusieurs champs | Une boîte, un champ chacun, dans l'ordre d'apparition dans la commande | | |
| 220 | +| Le même libellé deux fois | Un champ ; chaque occurrence reçoit ce qui y est tapé | | |
| 221 | +| Un libellé écrit des deux façons | Demandé une fois ; chaque occurrence honore ses propres accolades | | |
| 222 | +| Échap, ou Cancel | La commande ne s'exécute pas | | |
| 223 | +| Un champ laissé vide | Substitué par du vide — la commande émet sa propre plainte | | |
| 224 | +| Relancer l'outil | La boîte part de ce qui a été tapé la fois précédente, pour cette session seulement | | |
| 225 | +| Plus de champs qu'il n'en tient à l'écran | Refusé, avec un message disant combien tiennent | | |
| 226 | + | |
| 227 | +**Doubles accolades, pas simples.** `awk '{print $1}'` et `find . -exec rm {} +` sont des commandes ordinaires, et une syntaxe à accolades simples lirait la première comme une demande de valeur appelée `print $1`. | |
| 228 | + | |
| 229 | +Rien n'est écrit sur le disque. Une valeur que quelqu'un a tapée cet après-midi n'est pas une décision prise par le projet, elle ne va donc pas dans le répertoire propre du projet. | |
| 230 | + | |
| 231 | +### Erreurs | |
| 232 | + | |
| 233 | +| Erreur | Cause | | |
| 234 | +| --- | --- | | |
| 235 | +| `tool "X": "{{script" is never closed` | Un `{{` ouvrant sans `}}` après lui | | |
| 236 | +| `tool "X": {{}} asks for a value but does not say what it is` | Un champ sans libellé, ou qui n'est que `...` | | |
| 237 | + | |
| 238 | +Les deux sont refusés à la lecture du fichier, un champ à moitié tapé n'atteint donc jamais le shell avec ses accolades. | |
| 239 | + | |
| 240 | +## Voir aussi | |
| 241 | + | |
| 242 | +- [Lancer des commandes Golo depuis l'éditeur](../how-to/run-golo-commands.md) | |
| 243 | +- [Outils Golo](../explanation/golo-tools.md) | |
| 244 | +- [Fenêtres de terminal](terminal.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,244 @@ | |||
| 1 | +# Référence : outils Golo | ||
| 2 | + | ||
| 3 | +> Description neutre de `.turbo-golo/tools.toml`, du menu Golo, et de ce que fait l'exécution d'une commande. | ||
| 4 | + | ||
| 5 | +## Fichier | ||
| 6 | + | ||
| 7 | +| Propriété | Valeur | | ||
| 8 | +| --- | --- | | ||
| 9 | +| Chemin | `./.turbo-golo/tools.toml` | | ||
| 10 | +| Recherche | Le répertoire courant seulement. Les répertoires parents ne sont **pas** parcourus. | | ||
| 11 | +| Lecture | À chaque ouverture de l'un de ses menus, pour les entrées | | ||
| 12 | +| Relecture | Dès que la taille ou la date de modification du fichier change, pour l'**ensemble** des menus | | ||
| 13 | +| Fichier absent | Pas une erreur | | ||
| 14 | +| Fichier illisible | Une erreur, affichée dans le menu | | ||
| 15 | +| Fichier au niveau utilisateur | **Aucun.** Contrairement aux snippets, il n'y a pas de `~/.config/turbo-golo/tools.toml`. | | ||
| 16 | + | ||
| 17 | +## Format du fichier | ||
| 18 | + | ||
| 19 | +Une table `[[tool]]` par commande. | ||
| 20 | + | ||
| 21 | +| Clé | Type | Obligatoire | Description | | ||
| 22 | +| --- | --- | --- | --- | | ||
| 23 | +| `name` | chaîne | oui | Ce que le menu affiche. Peut porter une touche chaude entre tildes, comme `"~T~est"`. | | ||
| 24 | +| `command` | chaîne | oui | La commande shell à lancer | | ||
| 25 | +| `output` | chaîne | non | Où va sa sortie : `popup`, `terminal` ou `editor`. Absent signifie `popup`. | | ||
| 26 | +| `menu` | chaîne | non | Dans quel menu elle apparaît. Absent signifie `Golo`. N'importe quel nom ; le menu est créé pour vous. Peut porter une touche chaude entre tildes. | | ||
| 27 | + | ||
| 28 | +`menu` n'est pas vérifié contre une liste, parce qu'il n'y a pas de liste : un nom qu'aucun autre outil n'utilise crée simplement un menu. Un outil sans `name`, sans `command`, ou dont l'`output` nomme quelque chose qui n'existe pas rend tout le fichier erroné. Un `output` inconnu est **refusé plutôt que corrigé** : `"termnial"` aurait sinon l'air d'avoir marché tout en envoyant la sortie ailleurs. | ||
| 29 | + | ||
| 30 | +### Exemple | ||
| 31 | + | ||
| 32 | +```toml | ||
| 33 | +[[tool]] | ||
| 34 | +name = "~T~est" | ||
| 35 | +command = "golo --test" | ||
| 36 | +output = "popup" | ||
| 37 | + | ||
| 38 | +[[tool]] | ||
| 39 | +name = "~E~cho" | ||
| 40 | +command = "echo TADA" | ||
| 41 | +output = "terminal" | ||
| 42 | +menu = "Tools" | ||
| 43 | +``` | ||
| 44 | + | ||
| 45 | +## Le fichier de départ | ||
| 46 | + | ||
| 47 | +**Golo ▸ Create tools file** écrit ces neuf outils, dans cet ordre : | ||
| 48 | + | ||
| 49 | +| Nom | Commande | Sortie | Menu | | ||
| 50 | +| --- | --- | --- | --- | | ||
| 51 | +| `~R~un` | `golo {{script, e.g. main.golo}}` | `terminal` | Golo | | ||
| 52 | +| `~T~est` | `golo --test` | `popup` | Golo | | ||
| 53 | +| `Test ~o~ne` | `golo --test {{test file or directory}}` | `popup` | Golo | | ||
| 54 | +| `~D~ebug` | `golo --debug {{script, e.g. main.golo}}` | `terminal` | Golo | | ||
| 55 | +| `R~E~PL` | `golo` | `terminal` | Golo | | ||
| 56 | +| `~N~ew script` | `golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}` | `popup` | Golo | | ||
| 57 | +| `~B~uild native` | `gogolo build -o {{output executable}} {{script, e.g. main.golo}}` | `popup` | Golo | | ||
| 58 | +| `Build ~w~asm` | `wagolo build -target={{target: wasi, js or wasip2}} -o {{output .wasm}} {{script, e.g. main.golo}}` | `popup` | Golo | | ||
| 59 | +| `~E~cho` | `echo 🎉 tada!` | `terminal` | Tools | | ||
| 60 | + | ||
| 61 | +`Run` vient en premier parce que Golo est un langage de script et qu'exécuter le fichier est ce qu'un programmeur Golo fait le plus. Six d'entre eux demandent une valeur avant de s'exécuter — Golo n'a pas de manifeste, chaque commande qui touche un fichier doit donc se faire dire lequel — et un nomme un `menu` à lui. Ces deux fonctionnalités sont invisibles si le fichier de départ ne les montre pas. | ||
| 62 | + | ||
| 63 | +`Run`, `Debug` et `REPL` reçoivent un terminal : les deux premiers peuvent lire le clavier, et le troisième n'est rien d'autre. `Build native` a besoin de l'outillage Go sur le `PATH` ; `Build wasm` a besoin de TinyGo, et de `wasm-tools` pour la cible `wasip2`. | ||
| 64 | + | ||
| 65 | +Chaque outil nomme son `output`, y compris ceux qui nomment le défaut : la clé est la partie intéressante du format, et un fichier où elle apparaît une fois est un fichier où personne ne remarque qu'elle existe. | ||
| 66 | + | ||
| 67 | +L'entrée est grisée une fois que le projet a un fichier d'outils, elle ne peut donc pas en écraser un. Le fichier est écrit via un fichier temporaire dans le même répertoire, renommé en place. | ||
| 68 | + | ||
| 69 | +## Le menu Golo | ||
| 70 | + | ||
| 71 | +Toujours sur la barre, qu'un fichier d'outils existe ou non. Sa touche chaude est `Alt-G`. | ||
| 72 | + | ||
| 73 | +| Entrée | Condition | | ||
| 74 | +| --- | --- | | ||
| 75 | +| Une ligne par outil sans `menu`, dans l'ordre du fichier | Le fichier en contient au moins un | | ||
| 76 | +| `Cannot read tools`, grisé | Le fichier est présent mais illisible | | ||
| 77 | +| `Create tools file` | Le projet n'a pas de fichier d'outils | | ||
| 78 | +| `Open tools file` | Le projet en a un | | ||
| 79 | + | ||
| 80 | +## Les menus qu'un outil demande | ||
| 81 | + | ||
| 82 | +Un `menu` nommant autre chose que `Golo` met un menu de ce nom sur la barre. | ||
| 83 | + | ||
| 84 | +| Propriété | Valeur | | ||
| 85 | +| --- | --- | | ||
| 86 | +| Position | Entre Golo et Help | | ||
| 87 | +| Ordre | L'ordre dans lequel chaque nom apparaît pour la première fois dans le fichier | | ||
| 88 | +| Entrées | Une ligne par outil nommant ce menu, dans l'ordre du fichier. Rien d'autre — `Create tools file` et `Open tools file` restent dans Golo. | | ||
| 89 | +| Fichier illisible | Aucun menu du tout ; le menu Golo porte l'erreur | | ||
| 90 | +| Pendant que l'éditeur tourne | Ajoutés, retirés et renommés au fil des changements du fichier, sans redémarrage | | ||
| 91 | + | ||
| 92 | +### Touches chaudes | ||
| 93 | + | ||
| 94 | +Attribuées automatiquement, parce qu'un nom venu d'un fichier ne peut pas être vérifié à l'avance contre les menus fixes. | ||
| 95 | + | ||
| 96 | +| Cas | Résultat | | ||
| 97 | +| --- | --- | | ||
| 98 | +| Pas de tilde dans le nom | La première lettre qu'aucun autre menu n'a revendiquée est marquée. `Format` devient `For~m~at` : `F` est à File, `o` à Options, `r` à Run. | | ||
| 99 | +| Des tildes nommant une lettre libre | Conservés tels quels. `Doc~k~er` répond à `Alt-K`. | | ||
| 100 | +| Des tildes nommant une lettre prise | Abandonnés, et une lettre libre choisie à la place. `~F~oo` devient `F~o~o`. | | ||
| 101 | +| Toutes les lettres prises | Pas de touche chaude. `F10` et la souris l'ouvrent toujours. | | ||
| 102 | + | ||
| 103 | +Les lettres que tiennent les menus propres de l'éditeur sont `F`, `E`, `S`, `R`, `C`, `O`, `W`, `N` (Snippets), `G` (Golo) et `H`. | ||
| 104 | + | ||
| 105 | +## Exécuter une commande | ||
| 106 | + | ||
| 107 | +Commun à toutes les sorties : | ||
| 108 | + | ||
| 109 | +| Propriété | Valeur | | ||
| 110 | +| --- | --- | | ||
| 111 | +| Shell | `/bin/sh -c "<commande>"` sous Linux et macOS ; `cmd.exe /S /C "<commande>"` — le shell que nomme `%COMSPEC%` — sous Windows | | ||
| 112 | +| Répertoire | Le répertoire dans lequel l'éditeur a été lancé | | ||
| 113 | +| Sortie d'erreur | Fusionnée dans la sortie standard, dans l'ordre où la commande les a écrites | | ||
| 114 | + | ||
| 115 | +Passer par un shell signifie que les tubes, les globs, `&&` et `;` marchent tous, un outil peut donc être une séquence. Sous Windows le shell est cmd.exe, qui connaît `&&`, `|` et `>` mais ne développe pas les globs, et où `;` n'est pas un séparateur. | ||
| 116 | + | ||
| 117 | +### `output = "popup"` | ||
| 118 | + | ||
| 119 | +| Propriété | Valeur | | ||
| 120 | +| --- | --- | | ||
| 121 | +| S'ouvre | Immédiatement, avant que la commande soit finie | | ||
| 122 | +| Modal | Oui : rien d'autre dans l'éditeur n'est utilisable pendant qu'il est là | | ||
| 123 | +| Se remplit | À mesure que la sortie arrive, en la suivant jusqu'à ce que vous remontiez | | ||
| 124 | +| Titre pendant l'exécution | `<commande> — running` | | ||
| 125 | +| Titre une fois fini | `<commande> — ok`, ou `<commande> — exit <n>` | | ||
| 126 | +| Sortie vide, fini | Affiche `(no output)` | | ||
| 127 | +| Sortie vide, en cours | N'affiche rien | | ||
| 128 | +| Plafond de sortie | 10000 lignes ; au-delà les plus anciennes partent et une ligne `… n earlier lines dropped …` le dit | | ||
| 129 | + | ||
| 130 | +| Touche | Effet | | ||
| 131 | +| --- | --- | | ||
| 132 | +| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Parcourir la sortie | | ||
| 133 | +| Molette | Idem | | ||
| 134 | +| `Échap`, `Entrée`, **Close** | Le fermer, **en arrêtant la commande** si elle tourne encore | | ||
| 135 | + | ||
| 136 | +Fermer arrête la commande parce qu'il n'y a pas d'autre moyen d'interrompre une commande dont la sortie n'est pas dans un terminal. | ||
| 137 | + | ||
| 138 | +### `output = "terminal"` | ||
| 139 | + | ||
| 140 | +| Propriété | Valeur | | ||
| 141 | +| --- | --- | | ||
| 142 | +| Fenêtre | Une fenêtre de terminal à elle, titrée de la commande | | ||
| 143 | +| Environnement | Celui de l'éditeur, avec `TERM` à `xterm-256color` | | ||
| 144 | +| Après sa fin | La fenêtre reste, montrant sa sortie | | ||
| 145 | +| Modal | Non : l'éditeur continue à côté | | ||
| 146 | + | ||
| 147 | +Comme c'est un vrai terminal, les couleurs, la pagination, `Ctrl-C` et la lecture du clavier marchent tous — les coches vertes de `golo --test`, `readln` dans un script, l'invite du REPL. Voir [Fenêtres de terminal](terminal.md). | ||
| 148 | + | ||
| 149 | +Touches dans une fenêtre de terminal **terminée** : | ||
| 150 | + | ||
| 151 | +| Touche | Effet | | ||
| 152 | +| --- | --- | | ||
| 153 | +| `Shift-PgUp`, `Shift-PgDn` | Remonter dans la sortie | | ||
| 154 | +| `Ctrl-W` | Fermer la fenêtre | | ||
| 155 | +| Tout le reste | Atteint l'éditeur, pas le shell mort | | ||
| 156 | + | ||
| 157 | +### `output = "editor"` | ||
| 158 | + | ||
| 159 | +| Propriété | Valeur | | ||
| 160 | +| --- | --- | | ||
| 161 | +| Affiche | Un popup pendant l'exécution, comme ci-dessus | | ||
| 162 | +| À la fermeture du popup | Une fenêtre d'édition contenant la sortie, titrée de la commande | | ||
| 163 | +| Remplie | Une fois, quand la commande est finie — pas au fil de l'eau | | ||
| 164 | +| La fenêtre | Une fenêtre d'édition ordinaire sans nom de fichier : `Ctrl-F` la fouille, et `Save as` la conserve | | ||
| 165 | + | ||
| 166 | +## Rechargement après une commande | ||
| 167 | + | ||
| 168 | +Quand une commande se termine, chaque fichier ouvert est considéré. | ||
| 169 | + | ||
| 170 | +| Le fichier | Ce qui se passe | | ||
| 171 | +| --- | --- | | ||
| 172 | +| Non modifié, et changé sur le disque | Relu ; sa syntaxe est redécidée et son titre rafraîchi | | ||
| 173 | +| Non modifié, et inchangé sur le disque | Laissé tranquille, non compté | | ||
| 174 | +| Avec des modifications non enregistrées | Laissé tranquille et compté comme ignoré | | ||
| 175 | +| Jamais nommé | Laissé tranquille | | ||
| 176 | +| Disparu du disque | Laissé tranquille | | ||
| 177 | + | ||
| 178 | +Le curseur reste où il était, ramené dans ce que le fichier contient désormais. L'historique d'annulation est jeté, parce qu'annuler au-delà d'un rechargement restaurerait un texte que le fichier n'a plus. | ||
| 179 | + | ||
| 180 | +L'arbre du projet est rafraîchi au même instant — c'est ainsi qu'un fichier écrit par `golo new` y apparaît. | ||
| 181 | + | ||
| 182 | +| Barre d'état | Quand | | ||
| 183 | +| --- | --- | | ||
| 184 | +| `Running <commande>` | La fenêtre s'ouvre | | ||
| 185 | +| `Reloaded 2 files` | Deux fichiers ont été relus, aucun ignoré | | ||
| 186 | +| `Reloaded 2 files; 1 file with unsaved changes left alone` | Certains ont été ignorés | | ||
| 187 | +| `Command finished; 1 file with unsaved changes left alone` | Rien n'a été relu, quelque chose a été ignoré | | ||
| 188 | + | ||
| 189 | +## Erreurs | ||
| 190 | + | ||
| 191 | +| Message | Cause | | ||
| 192 | +| --- | --- | | ||
| 193 | +| `Cannot read tools` dans le menu | Le fichier est présent mais n'est pas du TOML valide, ou contient un outil sans nom ou sans commande | | ||
| 194 | +| `Already there: .turbo-golo/tools.toml` | Création dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; toujours possible pour un appelant qui n'est pas un menu. | | ||
| 195 | +| `This project has no .turbo-golo/tools.toml yet.` | Ouverture dans un projet qui n'en a pas, de même | | ||
| 196 | +| `Cannot tell which directory this is: …` | Le répertoire courant n'a pas pu être lu | | ||
| 197 | +| `Terminal windows are not supported on this platform yet` | Lancer une commande demande un pseudo-terminal, que Linux, macOS et Windows possèdent ; voir [Fenêtres de terminal](terminal.md) | | ||
| 198 | + | ||
| 199 | +## Demander une valeur | ||
| 200 | + | ||
| 201 | +Un `{{libellé}}` n'importe où dans une commande est une valeur que l'éditeur demande avant de l'exécuter, dans une boîte titrée du nom de l'outil. Le texte entre les accolades est ce que la boîte demande. | ||
| 202 | + | ||
| 203 | +| Écrit | Demandé | Substitué | | ||
| 204 | +| --- | --- | --- | | ||
| 205 | +| `{{script, e.g. main.golo}}` | `script, e.g. main.golo` | cité pour le shell | | ||
| 206 | +| `{{arguments...}}` | `arguments` | tel quel | | ||
| 207 | + | ||
| 208 | +Une valeur est **citée pour le shell** par défaut, un chemin avec un espace reste donc un seul argument. Un `...` final dans les accolades la demande telle quelle, et c'est ainsi qu'un champ peut valoir plusieurs arguments. | ||
| 209 | + | ||
| 210 | +```toml | ||
| 211 | +[[tool]] | ||
| 212 | +name = "Run with ~a~rguments" | ||
| 213 | +command = "golo main.golo {{arguments...}}" | ||
| 214 | +output = "terminal" | ||
| 215 | +``` | ||
| 216 | + | ||
| 217 | +| Règle | Comportement | | ||
| 218 | +| --- | --- | | ||
| 219 | +| Plusieurs champs | Une boîte, un champ chacun, dans l'ordre d'apparition dans la commande | | ||
| 220 | +| Le même libellé deux fois | Un champ ; chaque occurrence reçoit ce qui y est tapé | | ||
| 221 | +| Un libellé écrit des deux façons | Demandé une fois ; chaque occurrence honore ses propres accolades | | ||
| 222 | +| Échap, ou Cancel | La commande ne s'exécute pas | | ||
| 223 | +| Un champ laissé vide | Substitué par du vide — la commande émet sa propre plainte | | ||
| 224 | +| Relancer l'outil | La boîte part de ce qui a été tapé la fois précédente, pour cette session seulement | | ||
| 225 | +| Plus de champs qu'il n'en tient à l'écran | Refusé, avec un message disant combien tiennent | | ||
| 226 | + | ||
| 227 | +**Doubles accolades, pas simples.** `awk '{print $1}'` et `find . -exec rm {} +` sont des commandes ordinaires, et une syntaxe à accolades simples lirait la première comme une demande de valeur appelée `print $1`. | ||
| 228 | + | ||
| 229 | +Rien n'est écrit sur le disque. Une valeur que quelqu'un a tapée cet après-midi n'est pas une décision prise par le projet, elle ne va donc pas dans le répertoire propre du projet. | ||
| 230 | + | ||
| 231 | +### Erreurs | ||
| 232 | + | ||
| 233 | +| Erreur | Cause | | ||
| 234 | +| --- | --- | | ||
| 235 | +| `tool "X": "{{script" is never closed` | Un `{{` ouvrant sans `}}` après lui | | ||
| 236 | +| `tool "X": {{}} asks for a value but does not say what it is` | Un champ sans libellé, ou qui n'est que `...` | | ||
| 237 | + | ||
| 238 | +Les deux sont refusés à la lecture du fichier, un champ à moitié tapé n'atteint donc jamais le shell avec ses accolades. | ||
| 239 | + | ||
| 240 | +## Voir aussi | ||
| 241 | + | ||
| 242 | +- [Lancer des commandes Golo depuis l'éditeur](../how-to/run-golo-commands.md) | ||
| 243 | +- [Outils Golo](../explanation/golo-tools.md) | ||
| 244 | +- [Fenêtres de terminal](terminal.md) | ||
added
docs/fr/reference/keyboard.md +179 -0 | new file mode 100644 | ||
| @@ -0,0 +1,179 @@ | ||
| 1 | +# Référence : clavier | |
| 2 | + | |
| 3 | +> Liste complète des touches auxquelles Turbo Golo répond, regroupées par élément qui a le focus. | |
| 4 | + | |
| 5 | +Lorsque deux écritures existent, les deux fonctionnent : celle de Turbo C et la moderne. | |
| 6 | + | |
| 7 | +## Global | |
| 8 | + | |
| 9 | +Traitées où que soit le focus, sauf si un dialogue ou la liste de complétion est ouvert. | |
| 10 | + | |
| 11 | +| Touche | Action | | |
| 12 | +| --- | --- | | |
| 13 | +| `F1` | Décrire le symbole sous le curseur ; sans fichier ouvert, afficher l'aide clavier | | |
| 14 | +| `F2` | Enregistrer | | |
| 15 | +| `F3` | Ouvrir | | |
| 16 | +| `F4` | Nouveau | | |
| 17 | +| `F6` | Fenêtre suivante | | |
| 18 | +| `F7` | Occurrence suivante | | |
| 19 | +| `Maj-F7` | Occurrence précédente | | |
| 20 | +| `F8` | Ouvrir une fenêtre terminal | | |
| 21 | +| `F9` | Ouvrir l'arbre du projet | | |
| 22 | +| `F10` | Ouvrir la barre de menus | | |
| 23 | +| `F12` | Aller à la définition | | |
| 24 | +| `Shift-F12` | Trouver les références — avec `golo lsp`, la déclaration et chaque appel dans le fichier | | |
| 25 | +| `Ctrl-T` | Trouver un symbole dans tout le projet — avec `golo lsp`, dans chaque fichier `.golo` sous la racine du projet, ouvert ou non | | |
| 26 | +| `Ctrl-F` | Chercher | | |
| 27 | +| `Ctrl-G` | Aller à la ligne | | |
| 28 | +| `Ctrl-W` | Fermer la fenêtre courante | | |
| 29 | +| `Alt-X` | Quitter | | |
| 30 | +| `Alt-1` … `Alt-9` | Passer la fenêtre 1…9 au premier plan | | |
| 31 | +| `Alt-0` | Lister les fenêtres ouvertes | | |
| 32 | +| `Alt-N` | Ouvrir le menu Snippets | | |
| 33 | +| `Alt-G` | Ouvrir le menu Golo | | |
| 34 | +| `Alt-<lettre>` | Ouvrir le menu dont le titre porte cette lettre | | |
| 35 | + | |
| 36 | +Un menu ajouté par le fichier d'outils du projet reçoit sa lettre par attribution et non par choix, donc ce n'est jamais une de celles ci-dessus. Les règles sont dans [Outils Golo](golo-tools.md#touches-daccès). | |
| 37 | + | |
| 38 | +## Édition | |
| 39 | + | |
| 40 | +Traitées par la fenêtre qui a le focus. | |
| 41 | + | |
| 42 | +### Déplacement | |
| 43 | + | |
| 44 | +| Touche | Action | | |
| 45 | +| --- | --- | | |
| 46 | +| `←` `→` `↑` `↓` | Un caractère ou une ligne | | |
| 47 | +| `Ctrl-←` `Ctrl-→` | Début du mot précédent / suivant | | |
| 48 | +| `Origine` `Fin` | Début / fin de la ligne | | |
| 49 | +| `Ctrl-Origine` `Ctrl-Fin` | Début / fin du fichier | | |
| 50 | +| `Page↑` `Page↓` | Un écran | | |
| 51 | +| `Maj` + l'une des précédentes | Le même déplacement, en étendant la sélection | | |
| 52 | + | |
| 53 | +### Modification du texte | |
| 54 | + | |
| 55 | +| Touche | Action | | |
| 56 | +| --- | --- | | |
| 57 | +| tout caractère imprimable | L'insérer, en remplaçant la sélection | | |
| 58 | +| `Entrée` | Couper la ligne, en recopiant l'indentation de la ligne courante | | |
| 59 | +| `Retour arrière` | Supprimer la sélection, ou le caractère avant le curseur | | |
| 60 | +| `Suppr` | Supprimer la sélection, ou le caractère sous le curseur | | |
| 61 | +| `Tab` | Insérer une tabulation ; avec une sélection, indenter chaque ligne concernée | | |
| 62 | +| `Maj-Tab` | Retirer un niveau d'indentation de chaque ligne concernée | | |
| 63 | + | |
| 64 | +### Presse-papier et historique | |
| 65 | + | |
| 66 | +| Touche | Aussi | Action | | |
| 67 | +| --- | --- | --- | | |
| 68 | +| `Ctrl-C` | `Ctrl-Inser` | Copier la sélection | | |
| 69 | +| `Ctrl-X` | `Maj-Suppr` | Couper la sélection | | |
| 70 | +| `Ctrl-V` | `Maj-Inser` | Coller | | |
| 71 | +| `Ctrl-A` | | Tout sélectionner | | |
| 72 | +| `Ctrl-Z` | | Annuler | | |
| 73 | +| `Ctrl-R` | | Rétablir | | |
| 74 | +| `Ctrl-N` | | Insérer une ligne vide au-dessus du curseur | | |
| 75 | +| `Ctrl-Y` | | Supprimer la ligne où est le curseur | | |
| 76 | + | |
| 77 | +Une série de caractères tapés, ou une série de retours arrière, forme **une seule** étape d'annulation. Déplacer le curseur clôt la série. | |
| 78 | + | |
| 79 | +### Serveur de langage | |
| 80 | + | |
| 81 | +| Touche | Action | | |
| 82 | +| --- | --- | | |
| 83 | +| `Ctrl-Espace` | Demander une liste de complétion | | |
| 84 | +| `.` | Demander une liste de complétion, en effet de bord de la frappe | | |
| 85 | +| `F1` | Décrire le symbole sous le curseur | | |
| 86 | +| `F12` | Aller à la déclaration | | |
| 87 | + | |
| 88 | +Avec `golo lsp`, ces quatre-là fonctionnent, ainsi que `Shift-F12` et `Ctrl-T` listés plus haut — depuis GoloScript v0.2.0, qui annonce les références et les symboles du projet. | |
| 89 | + | |
| 90 | +## Barre de menus | |
| 91 | + | |
| 92 | +Une fois un menu ouvert. | |
| 93 | + | |
| 94 | +| Touche | Action | | |
| 95 | +| --- | --- | | |
| 96 | +| `←` `→` | Menu précédent / suivant | | |
| 97 | +| `↑` `↓` | Entrée précédente / suivante, en sautant les séparateurs et les entrées grisées | | |
| 98 | +| `Entrée` | Exécuter l'entrée surlignée | | |
| 99 | +| `<lettre>` | Exécuter l'entrée dont l'intitulé porte cette lettre | | |
| 100 | +| `Échap` | Fermer le menu | | |
| 101 | + | |
| 102 | +Toute autre touche est absorbée : une frappe égarée n'atteint jamais le fichier derrière. | |
| 103 | + | |
| 104 | +## Dialogues | |
| 105 | + | |
| 106 | +| Touche | Action | | |
| 107 | +| --- | --- | | |
| 108 | +| `Tab` / `Maj-Tab` | Contrôle suivant / précédent | | |
| 109 | +| `↑` `↓` | Parcourir la liste qui a le focus ; si le contrôle n'en a pas l'usage, contrôle suivant / précédent | | |
| 110 | +| `Entrée` | Actionner le bouton par défaut, d'où que soit le focus | | |
| 111 | +| `Échap` | Annuler | | |
| 112 | +| `Alt-<lettre>` | Actionner le bouton dont l'intitulé porte cette lettre | | |
| 113 | +| `Ctrl-U` | Vider le champ de saisie qui a le focus | | |
| 114 | + | |
| 115 | +Un dialogue est modal : toute touche dont il n'a pas l'usage est absorbée plutôt que transmise à l'éditeur derrière. | |
| 116 | + | |
| 117 | +### La boîte Open et Save As | |
| 118 | + | |
| 119 | +| | | | |
| 120 | +| --- | --- | | |
| 121 | +| Focus à l'ouverture | Le champ **Name**, pour pouvoir taper un nom directement. La première `↓` déplace donc le focus vers la liste ; la seconde déplace la surbrillance. | | |
| 122 | +| Déplacer la surbrillance | Place le nom de cette entrée dans le champ **Name** : le champ dit toujours ce sur quoi **OK** va agir. Surligner `../` le vide. | | |
| 123 | +| `Entrée` sur la liste | Ouvre le fichier surligné, ou entre dans le dossier surligné | | |
| 124 | +| **OK** | Agit sur le champ **Name** ; si le champ est vide, agit sur ce que la liste a surligné | | |
| 125 | +| Un nom qui est un dossier | Y entre au lieu de fermer le dialogue | | |
| 126 | +| Double clic | Équivaut à `Entrée` sur cette entrée | | |
| 127 | + | |
| 128 | +Les fichiers cachés ne sont pas listés. Les dossiers précèdent les fichiers, chaque groupe trié, avec `../` en premier. | |
| 129 | + | |
| 130 | +## Liste de complétion | |
| 131 | + | |
| 132 | +| Touche | Action | | |
| 133 | +| --- | --- | | |
| 134 | +| `↑` `↓` | Suggestion précédente / suivante | | |
| 135 | +| `Page↑` `Page↓` | Huit à la fois | | |
| 136 | +| `Entrée`, `Tab` | Accepter la suggestion surlignée | | |
| 137 | +| `Échap` | Abandonner la liste | | |
| 138 | +| tout caractère imprimable | Transmis à l'éditeur ; la liste se réduit à ce qui correspond encore | | |
| 139 | + | |
| 140 | +## Fenêtres terminal | |
| 141 | + | |
| 142 | +Une fenêtre terminal au premier plan reçoit **toutes les touches sauf** les touches de fonction, `Alt-X` et `Alt-0`…`Alt-9`, qui restent à l'éditeur pour qu'il y ait toujours une sortie hors d'un programme plein écran. `Ctrl-C`, `Ctrl-W`, `Ctrl-F` et `Alt-<lettre>` atteignent donc le shell plutôt que l'éditeur. | |
| 143 | + | |
| 144 | +| Touche | Action | | |
| 145 | +| --- | --- | | |
| 146 | +| `Maj-Page↑` `Maj-Page↓` | Reculer / avancer d'un écran dans l'historique | | |
| 147 | +| toute autre touche non réservée ci-dessus | Envoyée au shell, ramenant la vue à l'écran vivant | | |
| 148 | + | |
| 149 | +Les octets exacts envoyés par chaque touche sont dans [Fenêtres terminal](terminal.md). | |
| 150 | + | |
| 151 | +## Arbre du projet | |
| 152 | + | |
| 153 | +Traitées quand la fenêtre de l'arbre a le focus. Les règles complètes sont dans [Arbre du projet](project-tree.md). | |
| 154 | + | |
| 155 | +| Touche | Action | | |
| 156 | +| --- | --- | | |
| 157 | +| `↑` `↓` `Page↑` `Page↓` `Début` `Fin` | Déplacer la surbrillance | | |
| 158 | +| `→` | Déplier un dossier fermé, sinon aller à la ligne suivante | | |
| 159 | +| `←` | Replier un dossier ouvert, sinon remonter à son dossier | | |
| 160 | +| `Entrée` | Ouvrir un fichier ; déplier ou replier un dossier | | |
| 161 | +| `F5`, `Ctrl-R` | Relire le projet | | |
| 162 | + | |
| 163 | +## Souris | |
| 164 | + | |
| 165 | +| Action | Effet | | |
| 166 | +| --- | --- | | |
| 167 | +| Clic dans le texte | Placer le curseur | | |
| 168 | +| Glisser dans le texte | Sélectionner | | |
| 169 | +| Molette | Défiler de trois lignes | | |
| 170 | +| Clic sur un titre de menu | Ouvrir ou fermer ce menu | | |
| 171 | +| Clic sur un indice de la barre d'état | L'exécuter | | |
| 172 | +| Clic sur une fenêtre | La passer au premier plan | | |
| 173 | +| Glisser une barre de titre | Déplacer la fenêtre | | |
| 174 | +| Glisser le coin inférieur droit | Redimensionner la fenêtre | | |
| 175 | +| Clic sur `[x]` | Fermer la fenêtre | | |
| 176 | +| Clic sur `[■]` | Donner tout le bureau à la fenêtre | | |
| 177 | +| Clic sur `[▬]` | Remettre une fenêtre agrandie à sa taille précédente | | |
| 178 | +| Molette sur un terminal | Défiler de trois lignes dans son historique | | |
| 179 | +| Clic sur une ligne d'arbre | La surligner ; un second clic l'ouvre | | |
| new file mode 100644 | |||
| @@ -0,0 +1,179 @@ | |||
| 1 | +# Référence : clavier | ||
| 2 | + | ||
| 3 | +> Liste complète des touches auxquelles Turbo Golo répond, regroupées par élément qui a le focus. | ||
| 4 | + | ||
| 5 | +Lorsque deux écritures existent, les deux fonctionnent : celle de Turbo C et la moderne. | ||
| 6 | + | ||
| 7 | +## Global | ||
| 8 | + | ||
| 9 | +Traitées où que soit le focus, sauf si un dialogue ou la liste de complétion est ouvert. | ||
| 10 | + | ||
| 11 | +| Touche | Action | | ||
| 12 | +| --- | --- | | ||
| 13 | +| `F1` | Décrire le symbole sous le curseur ; sans fichier ouvert, afficher l'aide clavier | | ||
| 14 | +| `F2` | Enregistrer | | ||
| 15 | +| `F3` | Ouvrir | | ||
| 16 | +| `F4` | Nouveau | | ||
| 17 | +| `F6` | Fenêtre suivante | | ||
| 18 | +| `F7` | Occurrence suivante | | ||
| 19 | +| `Maj-F7` | Occurrence précédente | | ||
| 20 | +| `F8` | Ouvrir une fenêtre terminal | | ||
| 21 | +| `F9` | Ouvrir l'arbre du projet | | ||
| 22 | +| `F10` | Ouvrir la barre de menus | | ||
| 23 | +| `F12` | Aller à la définition | | ||
| 24 | +| `Shift-F12` | Trouver les références — avec `golo lsp`, la déclaration et chaque appel dans le fichier | | ||
| 25 | +| `Ctrl-T` | Trouver un symbole dans tout le projet — avec `golo lsp`, dans chaque fichier `.golo` sous la racine du projet, ouvert ou non | | ||
| 26 | +| `Ctrl-F` | Chercher | | ||
| 27 | +| `Ctrl-G` | Aller à la ligne | | ||
| 28 | +| `Ctrl-W` | Fermer la fenêtre courante | | ||
| 29 | +| `Alt-X` | Quitter | | ||
| 30 | +| `Alt-1` … `Alt-9` | Passer la fenêtre 1…9 au premier plan | | ||
| 31 | +| `Alt-0` | Lister les fenêtres ouvertes | | ||
| 32 | +| `Alt-N` | Ouvrir le menu Snippets | | ||
| 33 | +| `Alt-G` | Ouvrir le menu Golo | | ||
| 34 | +| `Alt-<lettre>` | Ouvrir le menu dont le titre porte cette lettre | | ||
| 35 | + | ||
| 36 | +Un menu ajouté par le fichier d'outils du projet reçoit sa lettre par attribution et non par choix, donc ce n'est jamais une de celles ci-dessus. Les règles sont dans [Outils Golo](golo-tools.md#touches-daccès). | ||
| 37 | + | ||
| 38 | +## Édition | ||
| 39 | + | ||
| 40 | +Traitées par la fenêtre qui a le focus. | ||
| 41 | + | ||
| 42 | +### Déplacement | ||
| 43 | + | ||
| 44 | +| Touche | Action | | ||
| 45 | +| --- | --- | | ||
| 46 | +| `←` `→` `↑` `↓` | Un caractère ou une ligne | | ||
| 47 | +| `Ctrl-←` `Ctrl-→` | Début du mot précédent / suivant | | ||
| 48 | +| `Origine` `Fin` | Début / fin de la ligne | | ||
| 49 | +| `Ctrl-Origine` `Ctrl-Fin` | Début / fin du fichier | | ||
| 50 | +| `Page↑` `Page↓` | Un écran | | ||
| 51 | +| `Maj` + l'une des précédentes | Le même déplacement, en étendant la sélection | | ||
| 52 | + | ||
| 53 | +### Modification du texte | ||
| 54 | + | ||
| 55 | +| Touche | Action | | ||
| 56 | +| --- | --- | | ||
| 57 | +| tout caractère imprimable | L'insérer, en remplaçant la sélection | | ||
| 58 | +| `Entrée` | Couper la ligne, en recopiant l'indentation de la ligne courante | | ||
| 59 | +| `Retour arrière` | Supprimer la sélection, ou le caractère avant le curseur | | ||
| 60 | +| `Suppr` | Supprimer la sélection, ou le caractère sous le curseur | | ||
| 61 | +| `Tab` | Insérer une tabulation ; avec une sélection, indenter chaque ligne concernée | | ||
| 62 | +| `Maj-Tab` | Retirer un niveau d'indentation de chaque ligne concernée | | ||
| 63 | + | ||
| 64 | +### Presse-papier et historique | ||
| 65 | + | ||
| 66 | +| Touche | Aussi | Action | | ||
| 67 | +| --- | --- | --- | | ||
| 68 | +| `Ctrl-C` | `Ctrl-Inser` | Copier la sélection | | ||
| 69 | +| `Ctrl-X` | `Maj-Suppr` | Couper la sélection | | ||
| 70 | +| `Ctrl-V` | `Maj-Inser` | Coller | | ||
| 71 | +| `Ctrl-A` | | Tout sélectionner | | ||
| 72 | +| `Ctrl-Z` | | Annuler | | ||
| 73 | +| `Ctrl-R` | | Rétablir | | ||
| 74 | +| `Ctrl-N` | | Insérer une ligne vide au-dessus du curseur | | ||
| 75 | +| `Ctrl-Y` | | Supprimer la ligne où est le curseur | | ||
| 76 | + | ||
| 77 | +Une série de caractères tapés, ou une série de retours arrière, forme **une seule** étape d'annulation. Déplacer le curseur clôt la série. | ||
| 78 | + | ||
| 79 | +### Serveur de langage | ||
| 80 | + | ||
| 81 | +| Touche | Action | | ||
| 82 | +| --- | --- | | ||
| 83 | +| `Ctrl-Espace` | Demander une liste de complétion | | ||
| 84 | +| `.` | Demander une liste de complétion, en effet de bord de la frappe | | ||
| 85 | +| `F1` | Décrire le symbole sous le curseur | | ||
| 86 | +| `F12` | Aller à la déclaration | | ||
| 87 | + | ||
| 88 | +Avec `golo lsp`, ces quatre-là fonctionnent, ainsi que `Shift-F12` et `Ctrl-T` listés plus haut — depuis GoloScript v0.2.0, qui annonce les références et les symboles du projet. | ||
| 89 | + | ||
| 90 | +## Barre de menus | ||
| 91 | + | ||
| 92 | +Une fois un menu ouvert. | ||
| 93 | + | ||
| 94 | +| Touche | Action | | ||
| 95 | +| --- | --- | | ||
| 96 | +| `←` `→` | Menu précédent / suivant | | ||
| 97 | +| `↑` `↓` | Entrée précédente / suivante, en sautant les séparateurs et les entrées grisées | | ||
| 98 | +| `Entrée` | Exécuter l'entrée surlignée | | ||
| 99 | +| `<lettre>` | Exécuter l'entrée dont l'intitulé porte cette lettre | | ||
| 100 | +| `Échap` | Fermer le menu | | ||
| 101 | + | ||
| 102 | +Toute autre touche est absorbée : une frappe égarée n'atteint jamais le fichier derrière. | ||
| 103 | + | ||
| 104 | +## Dialogues | ||
| 105 | + | ||
| 106 | +| Touche | Action | | ||
| 107 | +| --- | --- | | ||
| 108 | +| `Tab` / `Maj-Tab` | Contrôle suivant / précédent | | ||
| 109 | +| `↑` `↓` | Parcourir la liste qui a le focus ; si le contrôle n'en a pas l'usage, contrôle suivant / précédent | | ||
| 110 | +| `Entrée` | Actionner le bouton par défaut, d'où que soit le focus | | ||
| 111 | +| `Échap` | Annuler | | ||
| 112 | +| `Alt-<lettre>` | Actionner le bouton dont l'intitulé porte cette lettre | | ||
| 113 | +| `Ctrl-U` | Vider le champ de saisie qui a le focus | | ||
| 114 | + | ||
| 115 | +Un dialogue est modal : toute touche dont il n'a pas l'usage est absorbée plutôt que transmise à l'éditeur derrière. | ||
| 116 | + | ||
| 117 | +### La boîte Open et Save As | ||
| 118 | + | ||
| 119 | +| | | | ||
| 120 | +| --- | --- | | ||
| 121 | +| Focus à l'ouverture | Le champ **Name**, pour pouvoir taper un nom directement. La première `↓` déplace donc le focus vers la liste ; la seconde déplace la surbrillance. | | ||
| 122 | +| Déplacer la surbrillance | Place le nom de cette entrée dans le champ **Name** : le champ dit toujours ce sur quoi **OK** va agir. Surligner `../` le vide. | | ||
| 123 | +| `Entrée` sur la liste | Ouvre le fichier surligné, ou entre dans le dossier surligné | | ||
| 124 | +| **OK** | Agit sur le champ **Name** ; si le champ est vide, agit sur ce que la liste a surligné | | ||
| 125 | +| Un nom qui est un dossier | Y entre au lieu de fermer le dialogue | | ||
| 126 | +| Double clic | Équivaut à `Entrée` sur cette entrée | | ||
| 127 | + | ||
| 128 | +Les fichiers cachés ne sont pas listés. Les dossiers précèdent les fichiers, chaque groupe trié, avec `../` en premier. | ||
| 129 | + | ||
| 130 | +## Liste de complétion | ||
| 131 | + | ||
| 132 | +| Touche | Action | | ||
| 133 | +| --- | --- | | ||
| 134 | +| `↑` `↓` | Suggestion précédente / suivante | | ||
| 135 | +| `Page↑` `Page↓` | Huit à la fois | | ||
| 136 | +| `Entrée`, `Tab` | Accepter la suggestion surlignée | | ||
| 137 | +| `Échap` | Abandonner la liste | | ||
| 138 | +| tout caractère imprimable | Transmis à l'éditeur ; la liste se réduit à ce qui correspond encore | | ||
| 139 | + | ||
| 140 | +## Fenêtres terminal | ||
| 141 | + | ||
| 142 | +Une fenêtre terminal au premier plan reçoit **toutes les touches sauf** les touches de fonction, `Alt-X` et `Alt-0`…`Alt-9`, qui restent à l'éditeur pour qu'il y ait toujours une sortie hors d'un programme plein écran. `Ctrl-C`, `Ctrl-W`, `Ctrl-F` et `Alt-<lettre>` atteignent donc le shell plutôt que l'éditeur. | ||
| 143 | + | ||
| 144 | +| Touche | Action | | ||
| 145 | +| --- | --- | | ||
| 146 | +| `Maj-Page↑` `Maj-Page↓` | Reculer / avancer d'un écran dans l'historique | | ||
| 147 | +| toute autre touche non réservée ci-dessus | Envoyée au shell, ramenant la vue à l'écran vivant | | ||
| 148 | + | ||
| 149 | +Les octets exacts envoyés par chaque touche sont dans [Fenêtres terminal](terminal.md). | ||
| 150 | + | ||
| 151 | +## Arbre du projet | ||
| 152 | + | ||
| 153 | +Traitées quand la fenêtre de l'arbre a le focus. Les règles complètes sont dans [Arbre du projet](project-tree.md). | ||
| 154 | + | ||
| 155 | +| Touche | Action | | ||
| 156 | +| --- | --- | | ||
| 157 | +| `↑` `↓` `Page↑` `Page↓` `Début` `Fin` | Déplacer la surbrillance | | ||
| 158 | +| `→` | Déplier un dossier fermé, sinon aller à la ligne suivante | | ||
| 159 | +| `←` | Replier un dossier ouvert, sinon remonter à son dossier | | ||
| 160 | +| `Entrée` | Ouvrir un fichier ; déplier ou replier un dossier | | ||
| 161 | +| `F5`, `Ctrl-R` | Relire le projet | | ||
| 162 | + | ||
| 163 | +## Souris | ||
| 164 | + | ||
| 165 | +| Action | Effet | | ||
| 166 | +| --- | --- | | ||
| 167 | +| Clic dans le texte | Placer le curseur | | ||
| 168 | +| Glisser dans le texte | Sélectionner | | ||
| 169 | +| Molette | Défiler de trois lignes | | ||
| 170 | +| Clic sur un titre de menu | Ouvrir ou fermer ce menu | | ||
| 171 | +| Clic sur un indice de la barre d'état | L'exécuter | | ||
| 172 | +| Clic sur une fenêtre | La passer au premier plan | | ||
| 173 | +| Glisser une barre de titre | Déplacer la fenêtre | | ||
| 174 | +| Glisser le coin inférieur droit | Redimensionner la fenêtre | | ||
| 175 | +| Clic sur `[x]` | Fermer la fenêtre | | ||
| 176 | +| Clic sur `[■]` | Donner tout le bureau à la fenêtre | | ||
| 177 | +| Clic sur `[▬]` | Remettre une fenêtre agrandie à sa taille précédente | | ||
| 178 | +| Molette sur un terminal | Défiler de trois lignes dans son historique | | ||
| 179 | +| Clic sur une ligne d'arbre | La surligner ; un second clic l'ouvre | | ||
added
docs/fr/reference/languages.md +294 -0 | new file mode 100644 | ||
| @@ -0,0 +1,294 @@ | ||
| 1 | +# Référence : langages colorés | |
| 2 | + | |
| 3 | +> Description neutre des fichiers que Turbo Golo colore, de la façon dont il décide, et de ce que chaque scanner reconnaît. | |
| 4 | + | |
| 5 | +## Reconnaissance | |
| 6 | + | |
| 7 | +L'**extension** d'un fichier décide dès qu'elle est l'une de celles-ci : | |
| 8 | + | |
| 9 | +| Extension | Langage | | |
| 10 | +| --- | --- | | |
| 11 | +| `.golo` | Golo | | |
| 12 | +| `.toml` | TOML | | |
| 13 | +| `.yaml`, `.yml` | YAML | | |
| 14 | +| `.md`, `.markdown` | Markdown | | |
| 15 | +| `.js`, `.mjs`, `.cjs` | JavaScript | | |
| 16 | +| `.html`, `.htm` | HTML | | |
| 17 | +| `.xml`, `.xsd`, `.xsl`, `.xslt`, `.svg`, `.plist`, `.csproj`, `.pom` | XML | | |
| 18 | +| `.sh`, `.bash`, `.zsh` | Shell | | |
| 19 | +| `.dockerfile`, `.containerfile` | Dockerfile | | |
| 20 | + | |
| 21 | +Les extensions sont comparées sans tenir compte de la casse, et seule la dernière compte : `notes.golo.md` est du Markdown, et `main.golo.backup` n'est pas du Golo. | |
| 22 | + | |
| 23 | +Un fichier dont l'extension ne décide rien est ensuite cherché par son **nom**. Seuls les fichiers sans extension utile en ont besoin : | |
| 24 | + | |
| 25 | +| Nom | Langage | | |
| 26 | +| --- | --- | | |
| 27 | +| `Dockerfile`, `Containerfile` | Dockerfile | | |
| 28 | + | |
| 29 | +Un nom correspond sur sa totalité ou sur la partie avant le premier point, sans tenir compte de la casse — `Dockerfile`, `dockerfile` et `Dockerfile.dev` sont donc tous reconnus, tandis que `Dockerfile.md` est du Markdown, parce que l'extension est consultée d'abord. | |
| 30 | + | |
| 31 | +Un fichier que ni l'une ni l'autre table ne réclame est lu par sa **première ligne**. Un shebang nommant `golo` en fait du Golo : `#` ouvre un commentaire en Golo, l'interpréteur lit donc la ligne comme tel, et un script installé sans son extension et lancé comme une commande est du Golo et rien d'autre. Un shebang nommant un shell — `sh`, `bash`, `zsh`, `dash` ou `ksh` — en fait un script shell. L'interpréteur est reconnu comme élément de chemin ou comme argument d'`env`. | |
| 32 | + | |
| 33 | +| Première ligne | Résultat | | |
| 34 | +| --- | --- | | |
| 35 | +| `#!/usr/bin/env golo` | Golo | | |
| 36 | +| `#!/usr/local/bin/golo` | Golo | | |
| 37 | +| `#!/bin/sh` | Shell | | |
| 38 | +| `#!/usr/bin/env bash` | Shell | | |
| 39 | +| `#!/usr/bin/env -S bash -e` | Shell | | |
| 40 | +| `#!/usr/bin/env node` | Non coloré | | |
| 41 | +| Tout ce qui ne commence pas par `#!` | Non coloré | | |
| 42 | + | |
| 43 | +L'ordre est fixe — extension, puis nom, puis première ligne — et le premier qui décide l'emporte. | |
| 44 | + | |
| 45 | +Tout le reste est affiché en texte brut. Ce n'est pas une erreur — ouvrir un PNG dans l'éditeur n'est pas une faute, c'est juste non coloré. | |
| 46 | + | |
| 47 | +## Classes | |
| 48 | + | |
| 49 | +Chaque scanner produit le même vocabulaire de classes, et chacune correspond à une clé de thème. | |
| 50 | + | |
| 51 | +| Classe | Clé de thème | Produite par | | |
| 52 | +| --- | --- | --- | | |
| 53 | +| `identifier` | `syntax.identifier` | Golo, TOML, JavaScript, shell, YAML, Dockerfile | | |
| 54 | +| `keyword` | `syntax.keyword` | Golo, JavaScript, shell, HTML (doctype), XML, Dockerfile | | |
| 55 | +| `type` | `syntax.type` | Golo (noms capitalisés et chemins de module), TOML (en-têtes de table), YAML (tags) | | |
| 56 | +| `builtin` | `syntax.builtin` | Golo (les fonctions de l'interpréteur), JavaScript, shell (builtins et expansions), YAML (ancres et alias), Dockerfile (variables) | | |
| 57 | +| `constant` | `syntax.constant` | Golo, TOML, JavaScript, shell, YAML, HTML et XML (entités) | | |
| 58 | +| `function` | `syntax.function` | Golo, JavaScript, shell (la commande) | | |
| 59 | +| `string` | `syntax.string` | tous | | |
| 60 | +| `char` | `syntax.char` | Golo (`'c'`) | | |
| 61 | +| `number` | `syntax.number` | Golo, TOML, JavaScript, shell, YAML, Dockerfile | | |
| 62 | +| `comment` | `syntax.comment` | Golo, TOML, JavaScript, shell, HTML, YAML, XML, Dockerfile | | |
| 63 | +| `operator` | `syntax.operator` | Golo, TOML, JavaScript, shell, HTML, YAML (en-têtes de scalaires bloc), XML, Dockerfile | | |
| 64 | +| `punctuation` | `syntax.punctuation` | Golo, TOML, JavaScript, shell, Markdown, YAML, Dockerfile | | |
| 65 | +| `heading` | `syntax.heading` | Markdown | | |
| 66 | +| `tag` | `syntax.tag` | HTML, XML | | |
| 67 | +| `attribute` | `syntax.attribute` | HTML, XML, Dockerfile (options) | | |
| 68 | +| `emphasis` | `syntax.emphasis` | Markdown | | |
| 69 | +| `link` | `syntax.link` | Markdown | | |
| 70 | + | |
| 71 | +Golo ne produit aucune portée `heading`, `tag`, `attribute`, `emphasis` ou `link`. Dans `turbo-classic`, `syntax.number` et `syntax.constant` sont tous deux magenta et `syntax.char` a le vert de `syntax.string`, si bien que `42` et `true` partagent une couleur là, et `'x'` et `"x"` aussi ; d'autres thèmes les séparent. Voir [écrire son propre thème](../how-to/write-a-theme.md) pour changer cela. | |
| 72 | + | |
| 73 | +## Golo | |
| 74 | + | |
| 75 | +Écrit à la main, dans `internal/gololang`, contre `lexer/lexer.go` et `token/token.go` de GoloScript. Ce que le lexer lit comme un token, le scanner le colore comme une portée. | |
| 76 | + | |
| 77 | +**Quatre constructions franchissent une ligne**, et sont portées à la ligne suivante exactement comme le lexer les lit : un commentaire bloc `----` jusqu'à son `----` fermant ; une chaîne `"…"` jusqu'à son guillemet fermant ; une chaîne multiligne `"""…"""` jusqu'à ses trois guillemets fermants ; un littéral de caractère `'…'` jusqu'à son apostrophe fermante. Aucune des quatre ne s'imbrique. Une chaîne non terminée colore donc le reste du fichier, jusqu'à un guillemet — ce que l'interpréteur lit comme telle. | |
| 78 | + | |
| 79 | +| Reconnu | Comme | | |
| 80 | +| --- | --- | | |
| 81 | +| `and`, `augment`, `augmentation`, `await`, `break`, `case`, `catch`, `continue`, `else`, `finally`, `for`, `foreach`, `function`, `if`, `import`, `in`, `is`, `isnt`, `let`, `local`, `match`, `module`, `not`, `oftype`, `or`, `orIfNull`, `otherwise`, `return`, `spawn`, `struct`, `then`, `throw`, `try`, `union`, `var`, `when`, `while`, `with` | mot-clé | | |
| 82 | +| `true`, `false`, `null` | constante | | |
| 83 | +| les 157 builtins de l'interpréteur — `println`, `print`, `str`, `len`, `list`, `map`, `set`, `array`, `vector`, `range`, `readFile`, `toJSON`, `fromJSON`, `httpGet`, `DynamicObject`, … | builtin | | |
| 84 | +| tout autre nom commençant par une majuscule ASCII — `Point`, `Shape`, `Circle`, `Result_Failure`, `Some` | type | | |
| 85 | +| le chemin pointé après `module` ou `import` — `hello.World`, `gololang.Errors`, `java.util.List` — en une seule portée | type | | |
| 86 | +| le nom après `function` — `main` dans `function main = \|args\|` | fonction | | |
| 87 | +| tout autre nom en minuscules immédiatement avant `(` | fonction | | |
| 88 | +| tout autre nom : toute lettre ou marque Unicode, `_`, ou un emoji, puis des lettres, des chiffres et les mêmes — `x`, `été`, `名前`, `😀`, `🚀launch` | identifiant | | |
| 89 | +| `"…"` avec les échappements `\n \t \r \\ \" \' \0 \xHH`, sur plusieurs lignes | chaîne | | |
| 90 | +| `"""…"""`, sur plusieurs lignes, sans échappement | chaîne | | |
| 91 | +| `'…'` avec les mêmes échappements, sur plusieurs lignes | caractère | | |
| 92 | +| `42`, `3.14`, `1.5e-3`, `2E10`, `42L`, `3.14F`, `2.0f` | nombre | | |
| 93 | +| `#` jusqu'à la fin de la ligne, shebang compris | commentaire | | |
| 94 | +| `----` … `----`, sur plusieurs lignes | commentaire | | |
| 95 | +| `..`, `...` | opérateur | | |
| 96 | +| les suites de `+-*/%=<>!&\|^~?:` — dont `->`, `?:`, `==`, `!=`, `<=`, `>=` | opérateur | | |
| 97 | +| `()[]{},;` et un `.` seul | ponctuation | | |
| 98 | +| `$` dans `augment Shape$Circle` | ponctuation | | |
| 99 | + | |
| 100 | +**Un point rejoint un nombre seulement quand un chiffre le suit.** C'est le test du lexer lui-même, et c'est ce qui garde `1..3` comme un nombre et un intervalle plutôt que le double `1.` et un `.3` égaré. | |
| 101 | + | |
| 102 | +**Un exposant peut n'avoir aucun chiffre.** Le lexer lit `1e` comme un flottant et laisse le parseur se plaindre, `1e` est donc une seule portée de nombre. | |
| 103 | + | |
| 104 | +**Un nom capitalisé est un type par convention, pas par règle.** Golo vous laisse écrire `let Count = 1`, et il est coloré comme un type quand même. Les structs, les unions, les variantes et les cibles d'augmentation sont ce que les gens capitalisent, et la couleur suit les gens. | |
| 105 | + | |
| 106 | +**`Some`, `None`, `Ok` et `Err` sont des types, pas des constantes.** Ce sont les variantes d'unions ordinaires déclarées dans `gololang.Errors`, disponibles après un `import`, pas des builtins. | |
| 107 | + | |
| 108 | +**`DynamicObject` est un builtin, majuscule comprise.** Il est dans la table de l'interpréteur, et la table l'emporte sur la règle de casse. | |
| 109 | + | |
| 110 | +**Les noms peuvent contenir toute lettre Unicode, et des emoji.** Le `isLetter` du lexer admet les lettres, les marques, `_` et quatre blocs d'emoji (émoticônes, symboles et pictogrammes divers, symboles de transport et de cartes, symboles et pictogrammes supplémentaires) ; le scanner applique la même règle. | |
| 111 | + | |
| 112 | +**La coloration suit le lexer, pas le parseur.** Le parseur de GoloScript, en v0.1.1, refuse plusieurs tokens que son lexer lit : les suffixes `L`, `F` et `f`, un littéral de caractère `'c'`, l'intervalle `..`, `orIfNull`, `oftype` et `local function`. Ils sont colorés comme le lexer les lit, et le serveur de langage marque la ligne quand le parseur les refuse. `demos/syntax-tour/lexer-only.golo` en contient un de chaque. | |
| 113 | + | |
| 114 | +**Non reconnu**, chacun pour une raison énoncée : | |
| 115 | + | |
| 116 | +| Non reconnu | Parce que | | |
| 117 | +| --- | --- | | |
| 118 | +| `1_000` comme un seul nombre | Le lexer n'a pas de séparateur de chiffres : `_` commence un nom, c'est donc `1` puis `_000` | | |
| 119 | +| `0xFF`, `0b1010`, `0o17` comme nombres | Le lexer n'a pas de préfixe de base : `0xFF` est `0` puis le nom `xFF` | | |
| 120 | +| `.5` comme nombre | Le lexer exige un chiffre avant le point, c'est donc un point puis `5` | | |
| 121 | +| `42l` comme long | Le lexer n'accepte que le `L` majuscule, c'est donc `42` et le nom `l` | | |
| 122 | +| `---` comme commentaire | Quatre tirets ouvrent un commentaire bloc ; trois sont une suite d'opérateurs | | |
| 123 | +| Un mot-clé après `:` comme nom de méthode | `obj: match()` garde `match` en mot-clé — le scanner ne suit pas ce qu'un deux-points introduit | | |
| 124 | +| Un échappement dans `"""…"""` | Le lexer ajoute chaque rune jusqu'aux trois guillemets, `"""a\"""` se termine donc à son premier `"""` | | |
| 125 | +| Un constructeur autrement que comme un type | Rien dans la syntaxe ne sépare `Circle(1.0)` d'un type appliqué à des arguments | | |
| 126 | +| Une chaîne non terminée qui s'arrête à sa ligne | L'interpréteur lit jusqu'au guillemet fermant où qu'il soit, la couleur le suit donc — l'inverse de la décision de Turbo MoonBit, pour la raison inverse | | |
| 127 | +| Si un nom est lié dans cette portée | Rien ici ne lit plus d'une ligne à la fois ; c'est la question du serveur de langage, et [F1 y répond](../how-to/ask-about-code.md) | | |
| 128 | + | |
| 129 | +## TOML | |
| 130 | + | |
| 131 | +| Reconnu | Comme | | |
| 132 | +| --- | --- | | |
| 133 | +| `# commentaire` | commentaire | | |
| 134 | +| `[table]`, `[[tableau]]` | le nom comme type, les crochets comme ponctuation | | |
| 135 | +| `clé =` | identifiant, puis opérateur | | |
| 136 | +| `"basique"`, `'littérale'`, `"""multiligne"""`, `'''multiligne'''` | chaîne | | |
| 137 | +| `true`, `false` | constante | | |
| 138 | +| nombres, dates, heures, `inf`, `nan` | nombre | | |
| 139 | + | |
| 140 | +## YAML | |
| 141 | + | |
| 142 | +Un fichier compose, un manifeste Kubernetes et un workflow de CI sont tous cela : il n'y a pas de dialecte séparé, parce qu'un dialecte serait le schéma de quelqu'un d'autre à maintenir en phase. | |
| 143 | + | |
| 144 | +| Reconnu | Comme | | |
| 145 | +| --- | --- | | |
| 146 | +| `# commentaire` | commentaire | | |
| 147 | +| `clé:` avant un espace ou la fin de ligne | la clé comme identifiant, le deux-points comme ponctuation | | |
| 148 | +| `"citée": 1`, `'citée': 1` | la clé citée comme identifiant | | |
| 149 | +| `- ` ouvrant un élément de séquence | ponctuation | | |
| 150 | +| `"…"`, `'…'` | chaîne | | |
| 151 | +| `true`, `false`, `yes`, `no`, `on`, `off`, `null` | constante, quelle que soit la casse | | |
| 152 | +| nombres, dates et heures écrits sans guillemets | nombre | | |
| 153 | +| `&ancre`, `*alias` | builtin | | |
| 154 | +| `!!str`, `!Custom` | type | | |
| 155 | +| `---`, `...` | toute la ligne comme ponctuation | | |
| 156 | +| `{`, `}`, `[`, `]`, `,` | ponctuation | | |
| 157 | +| `\|`, `>`, avec leurs indicateurs de troncature et d'indentation | l'en-tête comme opérateur, le corps comme chaîne | | |
| 158 | + | |
| 159 | +**Un deux-points n'est un séparateur que si un espace ou la fin de ligne le suit.** `image: nginx:1.27` est une clé et une valeur, et `url: http://example.com/x` est une clé et une URL — colorer les deux-points intérieurs comme séparateurs mettrait chaque tag d'image et chaque URL en trois couleurs. | |
| 160 | + | |
| 161 | +**L'étendue d'un scalaire bloc est décidée par l'indentation**, pas par un délimiteur. La première ligne de contenu après `|` ou `>` fixe l'indentation du bloc ; chaque ligne indentée au moins autant lui appartient, et la première qui ne l'est pas le termine. **Une ligne vide dans un bloc reste dans le bloc** : un scalaire littéral garde ses lignes vides, et terminer le bloc au premier saut de paragraphe couperait en deux un script shell dans un fichier de CI. | |
| 162 | + | |
| 163 | +**Un `#` a besoin d'un espace devant pour ouvrir un commentaire**, `colour: ff#00aa` est donc un seul scalaire. | |
| 164 | + | |
| 165 | +| Non reconnu | Parce que | | |
| 166 | +| --- | --- | | |
| 167 | +| Le schéma d'un fichier compose, d'un manifeste ou d'un workflow | Colorer `services:` autrement que n'importe quelle clé signifie porter le schéma de quelqu'un d'autre, et il vieillit le jour où ils ajoutent une clé | | |
| 168 | +| Les flux multi-documents comme documents séparés | `---` est coloré, mais rien n'est réinitialisé ; rien dans la coloration ne dépend des frontières de documents | | |
| 169 | +| Si un mot nu est une chaîne ou un nombre pour un parseur | `1.2.3` est une version pour un lecteur et une chaîne pour YAML ; le scanner colore ce à quoi cela ressemble | | |
| 170 | + | |
| 171 | +## Markdown | |
| 172 | + | |
| 173 | +| Reconnu | Comme | | |
| 174 | +| --- | --- | | |
| 175 | +| `# Titre` … `###### Titre` | toute la ligne comme titre | | |
| 176 | +| `**gras**`, `__gras__`, `*italique*`, `_italique_` | emphase | | |
| 177 | +| `` `code` `` | chaîne | | |
| 178 | +| `[texte](cible)`, `` | le tout comme lien | | |
| 179 | +| `- `, `* `, `+ `, `1. `, `1) ` | le marqueur comme ponctuation | | |
| 180 | +| `>` | ponctuation | | |
| 181 | +| `---`, `***`, `___` | ponctuation | | |
| 182 | +| les clôtures ` ``` ` et `~~~` | tout le bloc, lignes d'ouverture et de fermeture comprises, comme chaîne | | |
| 183 | + | |
| 184 | +Un bloc clôturé est **d'une seule couleur quel que soit le langage annoncé** : ```` ```golo ```` ne colore pas son contenu comme du Golo. La suite de marqueurs qui ouvre un bloc doit être fermée par le même caractère, une clôture en accents graves n'est donc pas fermée par une en tildes. Une clôture non fermée colore jusqu'à la fin du fichier. | |
| 185 | + | |
| 186 | +La suite de marqueurs ouvrant une emphase doit être fermée par une suite de même longueur, `**gras**` est donc une portée plutôt que deux italiques. | |
| 187 | + | |
| 188 | +## JavaScript | |
| 189 | + | |
| 190 | +| Reconnu | Comme | | |
| 191 | +| --- | --- | | |
| 192 | +| `const`, `let`, `function`, `class`, `async`, `await`, `import`, `export`, … | mot-clé | | |
| 193 | +| `true`, `false`, `null`, `undefined`, `NaN`, `Infinity`, `this` | constante | | |
| 194 | +| `console`, `document`, `window`, `Array`, `Object`, `Promise`, `Math`, `JSON`, … | builtin | | |
| 195 | +| un nom immédiatement avant `(` | fonction | | |
| 196 | +| `"…"`, `'…'` | chaîne | | |
| 197 | +| `` `…` ``, interpolations comprises, sur plusieurs lignes | chaîne | | |
| 198 | +| `//` jusqu'à la fin de ligne, `/* … */` sur plusieurs lignes | commentaire | | |
| 199 | +| `42`, `3.14`, `0x1f`, `0b1010`, `0o777`, `1_000_000`, `1e6`, `10n` | nombre | | |
| 200 | +| les suites de `+-*/%=<>!&|^~?:` | opérateur | | |
| 201 | +| `()[]{},;.` | ponctuation | | |
| 202 | + | |
| 203 | +**Les littéraux d'expressions régulières ne sont pas reconnus.** Distinguer `/x/g` d'une division demande de savoir si le token précédent pouvait terminer une expression ; une mauvaise supposition colore le reste d'une ligne comme une chaîne, ce qui est pire que de laisser une regex de la couleur d'un opérateur. | |
| 204 | + | |
| 205 | +Les globales sont reconnues par leur nom, un fichier qui masque `Math` le voit donc toujours coloré comme builtin — la même règle que suivent ici les builtins de Golo. | |
| 206 | + | |
| 207 | +## HTML | |
| 208 | + | |
| 209 | +| Reconnu | Comme | | |
| 210 | +| --- | --- | | |
| 211 | +| `<tag`, `</tag`, `>`, `/>` | balise | | |
| 212 | +| les noms d'attributs, dont `data-*`, `xlink:href`, `@click`, `v-bind.prop` | attribut | | |
| 213 | +| `=` | opérateur | | |
| 214 | +| `"…"`, `'…'` | chaîne | | |
| 215 | +| `<!-- … -->`, sur plusieurs lignes | commentaire | | |
| 216 | +| `&`, `©` | constante | | |
| 217 | +| `<!DOCTYPE …>` et les autres déclarations | mot-clé | | |
| 218 | + | |
| 219 | +Le texte entre les balises n'est pas coloré. Un `&` nu sans `;` dans les 32 caractères est laissé tranquille, parce que c'est du texte légal. | |
| 220 | + | |
| 221 | +**Le contenu de `<script>` et `<style>` n'est pas coloré** comme du JavaScript et du CSS. | |
| 222 | + | |
| 223 | +## XML | |
| 224 | + | |
| 225 | +Son propre scanner plutôt que celui de HTML, pour une raison qui compte : CDATA. Tout l'intérêt de `<![CDATA[ … ]]>` est que son contenu n'est *pas* du balisage, et colorer les balises qu'il contient comme des balises est exactement à l'envers. | |
| 226 | + | |
| 227 | +| Reconnu | Comme | | |
| 228 | +| --- | --- | | |
| 229 | +| `<?xml version="1.0"?>` et les autres instructions de traitement | la cible et `?>` comme mot-clé, les paires entre elles comme attributs et chaînes | | |
| 230 | +| `<!DOCTYPE …>` et les autres formes `<!` | mot-clé | | |
| 231 | +| `<!-- … -->`, sur plusieurs lignes | commentaire | | |
| 232 | +| `<![CDATA[ … ]]>`, sur plusieurs lignes | chaîne | | |
| 233 | +| `<tag`, `</tag`, `>`, `/>` | balise | | |
| 234 | +| `<ns:tag>`, `xsi:type` | le préfixe et le nom local en **une** portée | | |
| 235 | +| les noms d'attributs | attribut | | |
| 236 | +| `=` | opérateur | | |
| 237 | +| `"…"`, `'…'` | chaîne | | |
| 238 | +| `&`, `©` | constante | | |
| 239 | + | |
| 240 | +**Un commentaire et une section CDATA se ferment sur des délimiteurs différents**, et sont portés séparément : un `-->` dans une section CDATA ne la termine pas. | |
| 241 | + | |
| 242 | +**Un `&` nu sans point-virgule dans les 32 caractères est laissé tranquille**, parce que c'est du texte légal dans bien des documents et qu'avaler le reste de la ligne serait la plus grosse erreur. | |
| 243 | + | |
| 244 | +Le texte entre les balises n'est pas coloré. | |
| 245 | + | |
| 246 | +## Shell | |
| 247 | + | |
| 248 | +Vaut pour `sh`, `bash` et `zsh` : les mots-clés reconnus sont ceux qu'ils partagent. | |
| 249 | + | |
| 250 | +| Reconnu | Comme | | |
| 251 | +| --- | --- | | |
| 252 | +| `if`, `then`, `fi`, `for`, `while`, `case`, `esac`, `function`, `return`, … | mot-clé | | |
| 253 | +| `true`, `false` | constante | | |
| 254 | +| `echo`, `printf`, `export`, `local`, `read`, `cd`, `set`, `source`, … | builtin | | |
| 255 | +| `$NAME`, `${…}`, `$(…)`, `$1`, `$?`, `$@` | builtin | | |
| 256 | +| le **premier mot nu d'une ligne** | fonction | | |
| 257 | +| chaque mot nu suivant, et `NAME` dans `NAME=valeur` | identifiant | | |
| 258 | +| `'…'`, sans rien d'échappé ni de développé dedans | chaîne | | |
| 259 | +| `"…"`, avec les expansions dedans colorées comme des expansions | chaîne | | |
| 260 | +| `#` jusqu'à la fin de ligne | commentaire | | |
| 261 | + | |
| 262 | +`$(a $(b) c)` est une seule portée : l'imbrication est comptée. Une option comme `-euo` est un seul mot, pas un moins et un mot. | |
| 263 | + | |
| 264 | +**Les heredocs ne sont pas reconnus.** `<<EOF` et le texte qui suit sont colorés comme du shell ordinaire. | |
| 265 | + | |
| 266 | +## Dockerfile | |
| 267 | + | |
| 268 | +| Reconnu | Comme | | |
| 269 | +| --- | --- | | |
| 270 | +| `FROM`, `RUN`, `COPY`, `ADD`, `ARG`, `ENV`, `CMD`, `ENTRYPOINT`, `EXPOSE`, `LABEL`, `USER`, `VOLUME`, `WORKDIR`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `MAINTAINER` | mot-clé, quelle que soit la casse | | |
| 271 | +| `AS`, `NONE` | mot-clé | | |
| 272 | +| `# commentaire`, y compris les directives `# syntax=` et `# escape=` | commentaire | | |
| 273 | +| `--from=builder`, `--chown=me:me` | le nom de l'option comme attribut | | |
| 274 | +| `$NAME`, `${NAME}`, `${NAME:-default}` | builtin, en une portée jusqu'à l'accolade fermante | | |
| 275 | +| `"…"`, `'…'` | chaîne | | |
| 276 | +| un `\` final | opérateur | | |
| 277 | +| les nombres | nombre | | |
| 278 | +| les chemins et références d'images — `/usr/local/bin`, `golang:1.26-alpine` | identifiant, en **une** portée | | |
| 279 | + | |
| 280 | +**Seul le premier mot d'une ligne peut être une instruction**, et un mot qui n'en est pas une est un argument — ce qui garde le premier mot d'une ligne de continuation hors de la couleur des mots-clés. | |
| 281 | + | |
| 282 | +**Rien ne franchit une ligne.** Un `\` joint deux lignes pour Docker, mais chaque moitié se lit toujours comme une commande et est colorée seule. | |
| 283 | + | |
| 284 | +| Non reconnu | Parce que | | |
| 285 | +| --- | --- | | |
| 286 | +| Le shell dans un `RUN` | Il faudrait lancer le scanner shell sur une partie de ligne et reporter ses colonnes, et `RUN` peut contenir n'importe quel langage | | |
| 287 | +| Les heredocs dans un `RUN` | La même raison que pour le scanner shell | | |
| 288 | +| Quelle étape un `--from` nomme | Rien ici ne lit le reste du fichier | | |
| 289 | + | |
| 290 | +## Voir aussi | |
| 291 | + | |
| 292 | +- [Format des fichiers de thème](themes.md) — chaque clé vers laquelle ces classes résolvent | |
| 293 | +- [Coloration et complétion](../explanation/colouring-and-completion.md) — pourquoi les scanners sont écrits ainsi | |
| 294 | +- [Écrire son propre thème](../how-to/write-a-theme.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,294 @@ | |||
| 1 | +# Référence : langages colorés | ||
| 2 | + | ||
| 3 | +> Description neutre des fichiers que Turbo Golo colore, de la façon dont il décide, et de ce que chaque scanner reconnaît. | ||
| 4 | + | ||
| 5 | +## Reconnaissance | ||
| 6 | + | ||
| 7 | +L'**extension** d'un fichier décide dès qu'elle est l'une de celles-ci : | ||
| 8 | + | ||
| 9 | +| Extension | Langage | | ||
| 10 | +| --- | --- | | ||
| 11 | +| `.golo` | Golo | | ||
| 12 | +| `.toml` | TOML | | ||
| 13 | +| `.yaml`, `.yml` | YAML | | ||
| 14 | +| `.md`, `.markdown` | Markdown | | ||
| 15 | +| `.js`, `.mjs`, `.cjs` | JavaScript | | ||
| 16 | +| `.html`, `.htm` | HTML | | ||
| 17 | +| `.xml`, `.xsd`, `.xsl`, `.xslt`, `.svg`, `.plist`, `.csproj`, `.pom` | XML | | ||
| 18 | +| `.sh`, `.bash`, `.zsh` | Shell | | ||
| 19 | +| `.dockerfile`, `.containerfile` | Dockerfile | | ||
| 20 | + | ||
| 21 | +Les extensions sont comparées sans tenir compte de la casse, et seule la dernière compte : `notes.golo.md` est du Markdown, et `main.golo.backup` n'est pas du Golo. | ||
| 22 | + | ||
| 23 | +Un fichier dont l'extension ne décide rien est ensuite cherché par son **nom**. Seuls les fichiers sans extension utile en ont besoin : | ||
| 24 | + | ||
| 25 | +| Nom | Langage | | ||
| 26 | +| --- | --- | | ||
| 27 | +| `Dockerfile`, `Containerfile` | Dockerfile | | ||
| 28 | + | ||
| 29 | +Un nom correspond sur sa totalité ou sur la partie avant le premier point, sans tenir compte de la casse — `Dockerfile`, `dockerfile` et `Dockerfile.dev` sont donc tous reconnus, tandis que `Dockerfile.md` est du Markdown, parce que l'extension est consultée d'abord. | ||
| 30 | + | ||
| 31 | +Un fichier que ni l'une ni l'autre table ne réclame est lu par sa **première ligne**. Un shebang nommant `golo` en fait du Golo : `#` ouvre un commentaire en Golo, l'interpréteur lit donc la ligne comme tel, et un script installé sans son extension et lancé comme une commande est du Golo et rien d'autre. Un shebang nommant un shell — `sh`, `bash`, `zsh`, `dash` ou `ksh` — en fait un script shell. L'interpréteur est reconnu comme élément de chemin ou comme argument d'`env`. | ||
| 32 | + | ||
| 33 | +| Première ligne | Résultat | | ||
| 34 | +| --- | --- | | ||
| 35 | +| `#!/usr/bin/env golo` | Golo | | ||
| 36 | +| `#!/usr/local/bin/golo` | Golo | | ||
| 37 | +| `#!/bin/sh` | Shell | | ||
| 38 | +| `#!/usr/bin/env bash` | Shell | | ||
| 39 | +| `#!/usr/bin/env -S bash -e` | Shell | | ||
| 40 | +| `#!/usr/bin/env node` | Non coloré | | ||
| 41 | +| Tout ce qui ne commence pas par `#!` | Non coloré | | ||
| 42 | + | ||
| 43 | +L'ordre est fixe — extension, puis nom, puis première ligne — et le premier qui décide l'emporte. | ||
| 44 | + | ||
| 45 | +Tout le reste est affiché en texte brut. Ce n'est pas une erreur — ouvrir un PNG dans l'éditeur n'est pas une faute, c'est juste non coloré. | ||
| 46 | + | ||
| 47 | +## Classes | ||
| 48 | + | ||
| 49 | +Chaque scanner produit le même vocabulaire de classes, et chacune correspond à une clé de thème. | ||
| 50 | + | ||
| 51 | +| Classe | Clé de thème | Produite par | | ||
| 52 | +| --- | --- | --- | | ||
| 53 | +| `identifier` | `syntax.identifier` | Golo, TOML, JavaScript, shell, YAML, Dockerfile | | ||
| 54 | +| `keyword` | `syntax.keyword` | Golo, JavaScript, shell, HTML (doctype), XML, Dockerfile | | ||
| 55 | +| `type` | `syntax.type` | Golo (noms capitalisés et chemins de module), TOML (en-têtes de table), YAML (tags) | | ||
| 56 | +| `builtin` | `syntax.builtin` | Golo (les fonctions de l'interpréteur), JavaScript, shell (builtins et expansions), YAML (ancres et alias), Dockerfile (variables) | | ||
| 57 | +| `constant` | `syntax.constant` | Golo, TOML, JavaScript, shell, YAML, HTML et XML (entités) | | ||
| 58 | +| `function` | `syntax.function` | Golo, JavaScript, shell (la commande) | | ||
| 59 | +| `string` | `syntax.string` | tous | | ||
| 60 | +| `char` | `syntax.char` | Golo (`'c'`) | | ||
| 61 | +| `number` | `syntax.number` | Golo, TOML, JavaScript, shell, YAML, Dockerfile | | ||
| 62 | +| `comment` | `syntax.comment` | Golo, TOML, JavaScript, shell, HTML, YAML, XML, Dockerfile | | ||
| 63 | +| `operator` | `syntax.operator` | Golo, TOML, JavaScript, shell, HTML, YAML (en-têtes de scalaires bloc), XML, Dockerfile | | ||
| 64 | +| `punctuation` | `syntax.punctuation` | Golo, TOML, JavaScript, shell, Markdown, YAML, Dockerfile | | ||
| 65 | +| `heading` | `syntax.heading` | Markdown | | ||
| 66 | +| `tag` | `syntax.tag` | HTML, XML | | ||
| 67 | +| `attribute` | `syntax.attribute` | HTML, XML, Dockerfile (options) | | ||
| 68 | +| `emphasis` | `syntax.emphasis` | Markdown | | ||
| 69 | +| `link` | `syntax.link` | Markdown | | ||
| 70 | + | ||
| 71 | +Golo ne produit aucune portée `heading`, `tag`, `attribute`, `emphasis` ou `link`. Dans `turbo-classic`, `syntax.number` et `syntax.constant` sont tous deux magenta et `syntax.char` a le vert de `syntax.string`, si bien que `42` et `true` partagent une couleur là, et `'x'` et `"x"` aussi ; d'autres thèmes les séparent. Voir [écrire son propre thème](../how-to/write-a-theme.md) pour changer cela. | ||
| 72 | + | ||
| 73 | +## Golo | ||
| 74 | + | ||
| 75 | +Écrit à la main, dans `internal/gololang`, contre `lexer/lexer.go` et `token/token.go` de GoloScript. Ce que le lexer lit comme un token, le scanner le colore comme une portée. | ||
| 76 | + | ||
| 77 | +**Quatre constructions franchissent une ligne**, et sont portées à la ligne suivante exactement comme le lexer les lit : un commentaire bloc `----` jusqu'à son `----` fermant ; une chaîne `"…"` jusqu'à son guillemet fermant ; une chaîne multiligne `"""…"""` jusqu'à ses trois guillemets fermants ; un littéral de caractère `'…'` jusqu'à son apostrophe fermante. Aucune des quatre ne s'imbrique. Une chaîne non terminée colore donc le reste du fichier, jusqu'à un guillemet — ce que l'interpréteur lit comme telle. | ||
| 78 | + | ||
| 79 | +| Reconnu | Comme | | ||
| 80 | +| --- | --- | | ||
| 81 | +| `and`, `augment`, `augmentation`, `await`, `break`, `case`, `catch`, `continue`, `else`, `finally`, `for`, `foreach`, `function`, `if`, `import`, `in`, `is`, `isnt`, `let`, `local`, `match`, `module`, `not`, `oftype`, `or`, `orIfNull`, `otherwise`, `return`, `spawn`, `struct`, `then`, `throw`, `try`, `union`, `var`, `when`, `while`, `with` | mot-clé | | ||
| 82 | +| `true`, `false`, `null` | constante | | ||
| 83 | +| les 157 builtins de l'interpréteur — `println`, `print`, `str`, `len`, `list`, `map`, `set`, `array`, `vector`, `range`, `readFile`, `toJSON`, `fromJSON`, `httpGet`, `DynamicObject`, … | builtin | | ||
| 84 | +| tout autre nom commençant par une majuscule ASCII — `Point`, `Shape`, `Circle`, `Result_Failure`, `Some` | type | | ||
| 85 | +| le chemin pointé après `module` ou `import` — `hello.World`, `gololang.Errors`, `java.util.List` — en une seule portée | type | | ||
| 86 | +| le nom après `function` — `main` dans `function main = \|args\|` | fonction | | ||
| 87 | +| tout autre nom en minuscules immédiatement avant `(` | fonction | | ||
| 88 | +| tout autre nom : toute lettre ou marque Unicode, `_`, ou un emoji, puis des lettres, des chiffres et les mêmes — `x`, `été`, `名前`, `😀`, `🚀launch` | identifiant | | ||
| 89 | +| `"…"` avec les échappements `\n \t \r \\ \" \' \0 \xHH`, sur plusieurs lignes | chaîne | | ||
| 90 | +| `"""…"""`, sur plusieurs lignes, sans échappement | chaîne | | ||
| 91 | +| `'…'` avec les mêmes échappements, sur plusieurs lignes | caractère | | ||
| 92 | +| `42`, `3.14`, `1.5e-3`, `2E10`, `42L`, `3.14F`, `2.0f` | nombre | | ||
| 93 | +| `#` jusqu'à la fin de la ligne, shebang compris | commentaire | | ||
| 94 | +| `----` … `----`, sur plusieurs lignes | commentaire | | ||
| 95 | +| `..`, `...` | opérateur | | ||
| 96 | +| les suites de `+-*/%=<>!&\|^~?:` — dont `->`, `?:`, `==`, `!=`, `<=`, `>=` | opérateur | | ||
| 97 | +| `()[]{},;` et un `.` seul | ponctuation | | ||
| 98 | +| `$` dans `augment Shape$Circle` | ponctuation | | ||
| 99 | + | ||
| 100 | +**Un point rejoint un nombre seulement quand un chiffre le suit.** C'est le test du lexer lui-même, et c'est ce qui garde `1..3` comme un nombre et un intervalle plutôt que le double `1.` et un `.3` égaré. | ||
| 101 | + | ||
| 102 | +**Un exposant peut n'avoir aucun chiffre.** Le lexer lit `1e` comme un flottant et laisse le parseur se plaindre, `1e` est donc une seule portée de nombre. | ||
| 103 | + | ||
| 104 | +**Un nom capitalisé est un type par convention, pas par règle.** Golo vous laisse écrire `let Count = 1`, et il est coloré comme un type quand même. Les structs, les unions, les variantes et les cibles d'augmentation sont ce que les gens capitalisent, et la couleur suit les gens. | ||
| 105 | + | ||
| 106 | +**`Some`, `None`, `Ok` et `Err` sont des types, pas des constantes.** Ce sont les variantes d'unions ordinaires déclarées dans `gololang.Errors`, disponibles après un `import`, pas des builtins. | ||
| 107 | + | ||
| 108 | +**`DynamicObject` est un builtin, majuscule comprise.** Il est dans la table de l'interpréteur, et la table l'emporte sur la règle de casse. | ||
| 109 | + | ||
| 110 | +**Les noms peuvent contenir toute lettre Unicode, et des emoji.** Le `isLetter` du lexer admet les lettres, les marques, `_` et quatre blocs d'emoji (émoticônes, symboles et pictogrammes divers, symboles de transport et de cartes, symboles et pictogrammes supplémentaires) ; le scanner applique la même règle. | ||
| 111 | + | ||
| 112 | +**La coloration suit le lexer, pas le parseur.** Le parseur de GoloScript, en v0.1.1, refuse plusieurs tokens que son lexer lit : les suffixes `L`, `F` et `f`, un littéral de caractère `'c'`, l'intervalle `..`, `orIfNull`, `oftype` et `local function`. Ils sont colorés comme le lexer les lit, et le serveur de langage marque la ligne quand le parseur les refuse. `demos/syntax-tour/lexer-only.golo` en contient un de chaque. | ||
| 113 | + | ||
| 114 | +**Non reconnu**, chacun pour une raison énoncée : | ||
| 115 | + | ||
| 116 | +| Non reconnu | Parce que | | ||
| 117 | +| --- | --- | | ||
| 118 | +| `1_000` comme un seul nombre | Le lexer n'a pas de séparateur de chiffres : `_` commence un nom, c'est donc `1` puis `_000` | | ||
| 119 | +| `0xFF`, `0b1010`, `0o17` comme nombres | Le lexer n'a pas de préfixe de base : `0xFF` est `0` puis le nom `xFF` | | ||
| 120 | +| `.5` comme nombre | Le lexer exige un chiffre avant le point, c'est donc un point puis `5` | | ||
| 121 | +| `42l` comme long | Le lexer n'accepte que le `L` majuscule, c'est donc `42` et le nom `l` | | ||
| 122 | +| `---` comme commentaire | Quatre tirets ouvrent un commentaire bloc ; trois sont une suite d'opérateurs | | ||
| 123 | +| Un mot-clé après `:` comme nom de méthode | `obj: match()` garde `match` en mot-clé — le scanner ne suit pas ce qu'un deux-points introduit | | ||
| 124 | +| Un échappement dans `"""…"""` | Le lexer ajoute chaque rune jusqu'aux trois guillemets, `"""a\"""` se termine donc à son premier `"""` | | ||
| 125 | +| Un constructeur autrement que comme un type | Rien dans la syntaxe ne sépare `Circle(1.0)` d'un type appliqué à des arguments | | ||
| 126 | +| Une chaîne non terminée qui s'arrête à sa ligne | L'interpréteur lit jusqu'au guillemet fermant où qu'il soit, la couleur le suit donc — l'inverse de la décision de Turbo MoonBit, pour la raison inverse | | ||
| 127 | +| Si un nom est lié dans cette portée | Rien ici ne lit plus d'une ligne à la fois ; c'est la question du serveur de langage, et [F1 y répond](../how-to/ask-about-code.md) | | ||
| 128 | + | ||
| 129 | +## TOML | ||
| 130 | + | ||
| 131 | +| Reconnu | Comme | | ||
| 132 | +| --- | --- | | ||
| 133 | +| `# commentaire` | commentaire | | ||
| 134 | +| `[table]`, `[[tableau]]` | le nom comme type, les crochets comme ponctuation | | ||
| 135 | +| `clé =` | identifiant, puis opérateur | | ||
| 136 | +| `"basique"`, `'littérale'`, `"""multiligne"""`, `'''multiligne'''` | chaîne | | ||
| 137 | +| `true`, `false` | constante | | ||
| 138 | +| nombres, dates, heures, `inf`, `nan` | nombre | | ||
| 139 | + | ||
| 140 | +## YAML | ||
| 141 | + | ||
| 142 | +Un fichier compose, un manifeste Kubernetes et un workflow de CI sont tous cela : il n'y a pas de dialecte séparé, parce qu'un dialecte serait le schéma de quelqu'un d'autre à maintenir en phase. | ||
| 143 | + | ||
| 144 | +| Reconnu | Comme | | ||
| 145 | +| --- | --- | | ||
| 146 | +| `# commentaire` | commentaire | | ||
| 147 | +| `clé:` avant un espace ou la fin de ligne | la clé comme identifiant, le deux-points comme ponctuation | | ||
| 148 | +| `"citée": 1`, `'citée': 1` | la clé citée comme identifiant | | ||
| 149 | +| `- ` ouvrant un élément de séquence | ponctuation | | ||
| 150 | +| `"…"`, `'…'` | chaîne | | ||
| 151 | +| `true`, `false`, `yes`, `no`, `on`, `off`, `null` | constante, quelle que soit la casse | | ||
| 152 | +| nombres, dates et heures écrits sans guillemets | nombre | | ||
| 153 | +| `&ancre`, `*alias` | builtin | | ||
| 154 | +| `!!str`, `!Custom` | type | | ||
| 155 | +| `---`, `...` | toute la ligne comme ponctuation | | ||
| 156 | +| `{`, `}`, `[`, `]`, `,` | ponctuation | | ||
| 157 | +| `\|`, `>`, avec leurs indicateurs de troncature et d'indentation | l'en-tête comme opérateur, le corps comme chaîne | | ||
| 158 | + | ||
| 159 | +**Un deux-points n'est un séparateur que si un espace ou la fin de ligne le suit.** `image: nginx:1.27` est une clé et une valeur, et `url: http://example.com/x` est une clé et une URL — colorer les deux-points intérieurs comme séparateurs mettrait chaque tag d'image et chaque URL en trois couleurs. | ||
| 160 | + | ||
| 161 | +**L'étendue d'un scalaire bloc est décidée par l'indentation**, pas par un délimiteur. La première ligne de contenu après `|` ou `>` fixe l'indentation du bloc ; chaque ligne indentée au moins autant lui appartient, et la première qui ne l'est pas le termine. **Une ligne vide dans un bloc reste dans le bloc** : un scalaire littéral garde ses lignes vides, et terminer le bloc au premier saut de paragraphe couperait en deux un script shell dans un fichier de CI. | ||
| 162 | + | ||
| 163 | +**Un `#` a besoin d'un espace devant pour ouvrir un commentaire**, `colour: ff#00aa` est donc un seul scalaire. | ||
| 164 | + | ||
| 165 | +| Non reconnu | Parce que | | ||
| 166 | +| --- | --- | | ||
| 167 | +| Le schéma d'un fichier compose, d'un manifeste ou d'un workflow | Colorer `services:` autrement que n'importe quelle clé signifie porter le schéma de quelqu'un d'autre, et il vieillit le jour où ils ajoutent une clé | | ||
| 168 | +| Les flux multi-documents comme documents séparés | `---` est coloré, mais rien n'est réinitialisé ; rien dans la coloration ne dépend des frontières de documents | | ||
| 169 | +| Si un mot nu est une chaîne ou un nombre pour un parseur | `1.2.3` est une version pour un lecteur et une chaîne pour YAML ; le scanner colore ce à quoi cela ressemble | | ||
| 170 | + | ||
| 171 | +## Markdown | ||
| 172 | + | ||
| 173 | +| Reconnu | Comme | | ||
| 174 | +| --- | --- | | ||
| 175 | +| `# Titre` … `###### Titre` | toute la ligne comme titre | | ||
| 176 | +| `**gras**`, `__gras__`, `*italique*`, `_italique_` | emphase | | ||
| 177 | +| `` `code` `` | chaîne | | ||
| 178 | +| `[texte](cible)`, `` | le tout comme lien | | ||
| 179 | +| `- `, `* `, `+ `, `1. `, `1) ` | le marqueur comme ponctuation | | ||
| 180 | +| `>` | ponctuation | | ||
| 181 | +| `---`, `***`, `___` | ponctuation | | ||
| 182 | +| les clôtures ` ``` ` et `~~~` | tout le bloc, lignes d'ouverture et de fermeture comprises, comme chaîne | | ||
| 183 | + | ||
| 184 | +Un bloc clôturé est **d'une seule couleur quel que soit le langage annoncé** : ```` ```golo ```` ne colore pas son contenu comme du Golo. La suite de marqueurs qui ouvre un bloc doit être fermée par le même caractère, une clôture en accents graves n'est donc pas fermée par une en tildes. Une clôture non fermée colore jusqu'à la fin du fichier. | ||
| 185 | + | ||
| 186 | +La suite de marqueurs ouvrant une emphase doit être fermée par une suite de même longueur, `**gras**` est donc une portée plutôt que deux italiques. | ||
| 187 | + | ||
| 188 | +## JavaScript | ||
| 189 | + | ||
| 190 | +| Reconnu | Comme | | ||
| 191 | +| --- | --- | | ||
| 192 | +| `const`, `let`, `function`, `class`, `async`, `await`, `import`, `export`, … | mot-clé | | ||
| 193 | +| `true`, `false`, `null`, `undefined`, `NaN`, `Infinity`, `this` | constante | | ||
| 194 | +| `console`, `document`, `window`, `Array`, `Object`, `Promise`, `Math`, `JSON`, … | builtin | | ||
| 195 | +| un nom immédiatement avant `(` | fonction | | ||
| 196 | +| `"…"`, `'…'` | chaîne | | ||
| 197 | +| `` `…` ``, interpolations comprises, sur plusieurs lignes | chaîne | | ||
| 198 | +| `//` jusqu'à la fin de ligne, `/* … */` sur plusieurs lignes | commentaire | | ||
| 199 | +| `42`, `3.14`, `0x1f`, `0b1010`, `0o777`, `1_000_000`, `1e6`, `10n` | nombre | | ||
| 200 | +| les suites de `+-*/%=<>!&|^~?:` | opérateur | | ||
| 201 | +| `()[]{},;.` | ponctuation | | ||
| 202 | + | ||
| 203 | +**Les littéraux d'expressions régulières ne sont pas reconnus.** Distinguer `/x/g` d'une division demande de savoir si le token précédent pouvait terminer une expression ; une mauvaise supposition colore le reste d'une ligne comme une chaîne, ce qui est pire que de laisser une regex de la couleur d'un opérateur. | ||
| 204 | + | ||
| 205 | +Les globales sont reconnues par leur nom, un fichier qui masque `Math` le voit donc toujours coloré comme builtin — la même règle que suivent ici les builtins de Golo. | ||
| 206 | + | ||
| 207 | +## HTML | ||
| 208 | + | ||
| 209 | +| Reconnu | Comme | | ||
| 210 | +| --- | --- | | ||
| 211 | +| `<tag`, `</tag`, `>`, `/>` | balise | | ||
| 212 | +| les noms d'attributs, dont `data-*`, `xlink:href`, `@click`, `v-bind.prop` | attribut | | ||
| 213 | +| `=` | opérateur | | ||
| 214 | +| `"…"`, `'…'` | chaîne | | ||
| 215 | +| `<!-- … -->`, sur plusieurs lignes | commentaire | | ||
| 216 | +| `&`, `©` | constante | | ||
| 217 | +| `<!DOCTYPE …>` et les autres déclarations | mot-clé | | ||
| 218 | + | ||
| 219 | +Le texte entre les balises n'est pas coloré. Un `&` nu sans `;` dans les 32 caractères est laissé tranquille, parce que c'est du texte légal. | ||
| 220 | + | ||
| 221 | +**Le contenu de `<script>` et `<style>` n'est pas coloré** comme du JavaScript et du CSS. | ||
| 222 | + | ||
| 223 | +## XML | ||
| 224 | + | ||
| 225 | +Son propre scanner plutôt que celui de HTML, pour une raison qui compte : CDATA. Tout l'intérêt de `<![CDATA[ … ]]>` est que son contenu n'est *pas* du balisage, et colorer les balises qu'il contient comme des balises est exactement à l'envers. | ||
| 226 | + | ||
| 227 | +| Reconnu | Comme | | ||
| 228 | +| --- | --- | | ||
| 229 | +| `<?xml version="1.0"?>` et les autres instructions de traitement | la cible et `?>` comme mot-clé, les paires entre elles comme attributs et chaînes | | ||
| 230 | +| `<!DOCTYPE …>` et les autres formes `<!` | mot-clé | | ||
| 231 | +| `<!-- … -->`, sur plusieurs lignes | commentaire | | ||
| 232 | +| `<![CDATA[ … ]]>`, sur plusieurs lignes | chaîne | | ||
| 233 | +| `<tag`, `</tag`, `>`, `/>` | balise | | ||
| 234 | +| `<ns:tag>`, `xsi:type` | le préfixe et le nom local en **une** portée | | ||
| 235 | +| les noms d'attributs | attribut | | ||
| 236 | +| `=` | opérateur | | ||
| 237 | +| `"…"`, `'…'` | chaîne | | ||
| 238 | +| `&`, `©` | constante | | ||
| 239 | + | ||
| 240 | +**Un commentaire et une section CDATA se ferment sur des délimiteurs différents**, et sont portés séparément : un `-->` dans une section CDATA ne la termine pas. | ||
| 241 | + | ||
| 242 | +**Un `&` nu sans point-virgule dans les 32 caractères est laissé tranquille**, parce que c'est du texte légal dans bien des documents et qu'avaler le reste de la ligne serait la plus grosse erreur. | ||
| 243 | + | ||
| 244 | +Le texte entre les balises n'est pas coloré. | ||
| 245 | + | ||
| 246 | +## Shell | ||
| 247 | + | ||
| 248 | +Vaut pour `sh`, `bash` et `zsh` : les mots-clés reconnus sont ceux qu'ils partagent. | ||
| 249 | + | ||
| 250 | +| Reconnu | Comme | | ||
| 251 | +| --- | --- | | ||
| 252 | +| `if`, `then`, `fi`, `for`, `while`, `case`, `esac`, `function`, `return`, … | mot-clé | | ||
| 253 | +| `true`, `false` | constante | | ||
| 254 | +| `echo`, `printf`, `export`, `local`, `read`, `cd`, `set`, `source`, … | builtin | | ||
| 255 | +| `$NAME`, `${…}`, `$(…)`, `$1`, `$?`, `$@` | builtin | | ||
| 256 | +| le **premier mot nu d'une ligne** | fonction | | ||
| 257 | +| chaque mot nu suivant, et `NAME` dans `NAME=valeur` | identifiant | | ||
| 258 | +| `'…'`, sans rien d'échappé ni de développé dedans | chaîne | | ||
| 259 | +| `"…"`, avec les expansions dedans colorées comme des expansions | chaîne | | ||
| 260 | +| `#` jusqu'à la fin de ligne | commentaire | | ||
| 261 | + | ||
| 262 | +`$(a $(b) c)` est une seule portée : l'imbrication est comptée. Une option comme `-euo` est un seul mot, pas un moins et un mot. | ||
| 263 | + | ||
| 264 | +**Les heredocs ne sont pas reconnus.** `<<EOF` et le texte qui suit sont colorés comme du shell ordinaire. | ||
| 265 | + | ||
| 266 | +## Dockerfile | ||
| 267 | + | ||
| 268 | +| Reconnu | Comme | | ||
| 269 | +| --- | --- | | ||
| 270 | +| `FROM`, `RUN`, `COPY`, `ADD`, `ARG`, `ENV`, `CMD`, `ENTRYPOINT`, `EXPOSE`, `LABEL`, `USER`, `VOLUME`, `WORKDIR`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `MAINTAINER` | mot-clé, quelle que soit la casse | | ||
| 271 | +| `AS`, `NONE` | mot-clé | | ||
| 272 | +| `# commentaire`, y compris les directives `# syntax=` et `# escape=` | commentaire | | ||
| 273 | +| `--from=builder`, `--chown=me:me` | le nom de l'option comme attribut | | ||
| 274 | +| `$NAME`, `${NAME}`, `${NAME:-default}` | builtin, en une portée jusqu'à l'accolade fermante | | ||
| 275 | +| `"…"`, `'…'` | chaîne | | ||
| 276 | +| un `\` final | opérateur | | ||
| 277 | +| les nombres | nombre | | ||
| 278 | +| les chemins et références d'images — `/usr/local/bin`, `golang:1.26-alpine` | identifiant, en **une** portée | | ||
| 279 | + | ||
| 280 | +**Seul le premier mot d'une ligne peut être une instruction**, et un mot qui n'en est pas une est un argument — ce qui garde le premier mot d'une ligne de continuation hors de la couleur des mots-clés. | ||
| 281 | + | ||
| 282 | +**Rien ne franchit une ligne.** Un `\` joint deux lignes pour Docker, mais chaque moitié se lit toujours comme une commande et est colorée seule. | ||
| 283 | + | ||
| 284 | +| Non reconnu | Parce que | | ||
| 285 | +| --- | --- | | ||
| 286 | +| Le shell dans un `RUN` | Il faudrait lancer le scanner shell sur une partie de ligne et reporter ses colonnes, et `RUN` peut contenir n'importe quel langage | | ||
| 287 | +| Les heredocs dans un `RUN` | La même raison que pour le scanner shell | | ||
| 288 | +| Quelle étape un `--from` nomme | Rien ici ne lit le reste du fichier | | ||
| 289 | + | ||
| 290 | +## Voir aussi | ||
| 291 | + | ||
| 292 | +- [Format des fichiers de thème](themes.md) — chaque clé vers laquelle ces classes résolvent | ||
| 293 | +- [Coloration et complétion](../explanation/colouring-and-completion.md) — pourquoi les scanners sont écrits ainsi | ||
| 294 | +- [Écrire son propre thème](../how-to/write-a-theme.md) | ||
added
docs/fr/reference/project-settings.md +111 -0 | new file mode 100644 | ||
| @@ -0,0 +1,111 @@ | ||
| 1 | +# Référence : réglages de projet | |
| 2 | + | |
| 3 | +> Description neutre de `.turbo-golo/settings.toml` : où il est cherché, ce qu'il peut contenir, et ce qui y écrit. | |
| 4 | + | |
| 5 | +## Emplacement | |
| 6 | + | |
| 7 | +| Propriété | Valeur | | |
| 8 | +| --- | --- | | |
| 9 | +| Dossier | `.turbo-golo` dans le répertoire de travail de l'éditeur | | |
| 10 | +| Fichier | `.turbo-golo/settings.toml` | | |
| 11 | +| Recherche | Le répertoire de travail seulement. Les dossiers parents ne sont **pas** parcourus. | | |
| 12 | +| Lecture | Au démarrage de l'éditeur, puis à chaque enregistrement du fichier depuis l'éditeur | | |
| 13 | +| Obligatoire | Non. Un projet qui n'en a pas obtient les valeurs par défaut ci-dessous. | | |
| 14 | + | |
| 15 | +## Clés | |
| 16 | + | |
| 17 | +Chaque clé est facultative, et chaque clé se trouve dans la table `[editor]`. Une clé absente garde sa valeur par défaut ; une clé présente l'emporte, y compris avec une valeur égale à la valeur par défaut. | |
| 18 | + | |
| 19 | +| Clé | Type | Défaut | Description | | |
| 20 | +| --- | --- | --- | --- | | |
| 21 | +| `theme` | chaîne | le défaut de l'éditeur (`turbo-classic`) | Nom du thème de couleurs de démarrage, tel que listé par `turbo-golo -list-themes` | | |
| 22 | +| `autosave` | booléen | `false` | Si les fichiers modifiés sont écrits sans qu'on le demande. Le fichier qu'écrit **Create project settings** le met à `true` ; la valeur par défaut ici est celle qui s'applique à un projet sans fichier de réglages du tout. | | |
| 23 | +| `autosave_delay` | chaîne | `"2s"` | Combien de temps attendre après la dernière frappe. Une durée Go : `"500ms"`, `"2s"`, `"1m"`. Consultée seulement quand `autosave` vaut true. | | |
| 24 | + | |
| 25 | +### Exemple | |
| 26 | + | |
| 27 | +```toml | |
| 28 | +[editor] | |
| 29 | +theme = "turbo-dark" | |
| 30 | +autosave = true | |
| 31 | +autosave_delay = "500ms" | |
| 32 | +``` | |
| 33 | + | |
| 34 | +## Priorité du thème | |
| 35 | + | |
| 36 | +De la plus forte à la plus faible : | |
| 37 | + | |
| 38 | +| Source | L'emporte sur | | |
| 39 | +| --- | --- | | |
| 40 | +| `-theme` sur la ligne de commande | tout | | |
| 41 | +| `theme` dans le fichier de réglages | le défaut intégré | | |
| 42 | +| Le défaut intégré `turbo-classic` | — | | |
| 43 | + | |
| 44 | +Un nom de thème inconnu, à n'importe quel niveau, retombe sur le défaut intégré plutôt que d'échouer. | |
| 45 | + | |
| 46 | +## Quand un changement prend effet | |
| 47 | + | |
| 48 | +Le fichier est lu au démarrage, et **de nouveau à chaque enregistrement depuis l'éditeur** — un changement fait dans l'éditeur est donc en vigueur dès que vous appuyez sur `F2`, sans redémarrage. | |
| 49 | + | |
| 50 | +| Clé | Ré-appliquée à l'enregistrement | Pourquoi | | |
| 51 | +| --- | --- | --- | | |
| 52 | +| `autosave` | oui | | | |
| 53 | +| `autosave_delay` | oui | | | |
| 54 | +| `theme` | **non** | Options ▸ Theme est la façon vivante de le changer, et y réécrit déjà le choix. Une option `-theme` donnée en ligne de commande est l'affirmation la plus explicite pour cette session, et l'enregistrement d'un fichier ne la contredit pas. | | |
| 55 | + | |
| 56 | +| Résultat | Barre d'état | | |
| 57 | +| --- | --- | | |
| 58 | +| Lu et appliqué | `Applied .turbo-golo/settings.toml — autosave on (2s)` | | |
| 59 | +| Lu et appliqué, autosave désactivée | `Applied .turbo-golo/settings.toml — autosave off` | | |
| 60 | +| Enregistré, mais plus du TOML valide | `Saved, but not applied: …` — les valeurs précédentes restent en vigueur | | |
| 61 | + | |
| 62 | +Enregistrer est enregistrer, quel qu'en soit l'auteur : la sauvegarde automatique qui écrit le fichier de réglages les ré-applique exactement comme `F2`. Une modification faite **hors** de l'éditeur n'est pas remarquée ; rien ne surveille le fichier. | |
| 63 | + | |
| 64 | +## Sauvegarde automatique | |
| 65 | + | |
| 66 | +| Comportement | Détail | | |
| 67 | +| --- | --- | | |
| 68 | +| Déclencheur | Le délai écoulé sans modification dans aucune fenêtre | | |
| 69 | +| Portée | Tout fichier ouvert qui a un nom, pas seulement celui au premier plan | | |
| 70 | +| Échéance | Une seule pour tout l'éditeur, relancée par toute modification dans n'importe quelle fenêtre | | |
| 71 | +| Fichiers sans nom | Jamais enregistrés ; jamais l'objet d'une question | | |
| 72 | +| Signalement | `Saved <nom>` dans la barre d'état | | |
| 73 | +| Échec | Signalé dans la barre d'état, jamais dans un dialogue, et non réessayé avant la modification suivante | | |
| 74 | +| Fermeture d'une fenêtre | Enregistre au lieu de demander, si le fichier a un nom | | |
| 75 | +| Sortie de l'éditeur | Enregistre au lieu de demander, si le fichier a un nom | | |
| 76 | + | |
| 77 | +## Écritures | |
| 78 | + | |
| 79 | +Le fichier de réglages est écrit par exactement deux actions. Rien d'autre dans l'éditeur n'y écrit, et rien ne le crée tout seul. | |
| 80 | + | |
| 81 | +| Action | Effet | | |
| 82 | +| --- | --- | | |
| 83 | +| **Options ▸ Create project settings** | Crée `.turbo-golo/settings.toml` avec le thème en cours, `autosave = true` et des commentaires explicatifs. Grisée dès que le projet en a un, elle ne peut donc pas être choisie deux fois. | | |
| 84 | +| **Options ▸ Theme** | Réécrit la valeur de `theme` **uniquement si le fichier existe déjà**. Commentaires, lignes vides, ordre des clés et commentaire de fin de la ligne du thème sont conservés. | | |
| 85 | + | |
| 86 | +Les deux écrivent via un fichier temporaire du même dossier, renommé en place : une écriture interrompue laisse le fichier précédent intact. | |
| 87 | + | |
| 88 | +## Entrées de menu | |
| 89 | + | |
| 90 | +| Entrée | Menu | Fichier requis | Effet | | |
| 91 | +| --- | --- | --- | --- | | |
| 92 | +| Create project settings | Options | refuse le fichier | Comme ci-dessus, puis ouvre le fichier. Grisée dès que le projet en a un. | | |
| 93 | +| Project settings… | Options | exige le fichier | Ouvre `.turbo-golo/settings.toml`. Grisée tant que le projet n'en a pas. | | |
| 94 | + | |
| 95 | +## Erreurs | |
| 96 | + | |
| 97 | +| Message | Cause | | |
| 98 | +| --- | --- | | |
| 99 | +| `turbo-golo: reading …/settings.toml: …` sur la sortie d'erreur | Le fichier est là mais n'est pas du TOML valide. L'éditeur s'ouvre avec ses valeurs par défaut. | | |
| 100 | +| `reading …: autosave_delay "x" is not a duration such as "2s"` | `autosave_delay` n'est pas une durée Go | | |
| 101 | +| `reading …: autosave_delay must be positive, not "0s"` | `autosave_delay` est nul ou négatif | | |
| 102 | +| `Already there: .turbo-golo/settings.toml` dans la barre d'état | Créer dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; reste possible pour un appelant qui n'est pas un menu. | | |
| 103 | +| `Saved, but not applied: …` dans la barre d'état | Le fichier de réglages a été écrit mais ne s'analyse plus. Les valeurs précédentes restent en vigueur. | | |
| 104 | +| `This project has no .turbo-golo/settings.toml yet.` | **Project settings…** dans un projet qui n'en a pas | | |
| 105 | +| `Theme set for this session only: …` | Le thème a changé mais le fichier de réglages n'a pas pu être écrit | | |
| 106 | + | |
| 107 | +## Voir aussi | |
| 108 | + | |
| 109 | +- [Donner ses propres réglages à un projet](../how-to/configure-a-project.md) | |
| 110 | +- [Réglages de projet](../explanation/project-settings.md) | |
| 111 | +- [Format des fichiers de thème](themes.md) — un autre fichier, dans le même langage | |
| new file mode 100644 | |||
| @@ -0,0 +1,111 @@ | |||
| 1 | +# Référence : réglages de projet | ||
| 2 | + | ||
| 3 | +> Description neutre de `.turbo-golo/settings.toml` : où il est cherché, ce qu'il peut contenir, et ce qui y écrit. | ||
| 4 | + | ||
| 5 | +## Emplacement | ||
| 6 | + | ||
| 7 | +| Propriété | Valeur | | ||
| 8 | +| --- | --- | | ||
| 9 | +| Dossier | `.turbo-golo` dans le répertoire de travail de l'éditeur | | ||
| 10 | +| Fichier | `.turbo-golo/settings.toml` | | ||
| 11 | +| Recherche | Le répertoire de travail seulement. Les dossiers parents ne sont **pas** parcourus. | | ||
| 12 | +| Lecture | Au démarrage de l'éditeur, puis à chaque enregistrement du fichier depuis l'éditeur | | ||
| 13 | +| Obligatoire | Non. Un projet qui n'en a pas obtient les valeurs par défaut ci-dessous. | | ||
| 14 | + | ||
| 15 | +## Clés | ||
| 16 | + | ||
| 17 | +Chaque clé est facultative, et chaque clé se trouve dans la table `[editor]`. Une clé absente garde sa valeur par défaut ; une clé présente l'emporte, y compris avec une valeur égale à la valeur par défaut. | ||
| 18 | + | ||
| 19 | +| Clé | Type | Défaut | Description | | ||
| 20 | +| --- | --- | --- | --- | | ||
| 21 | +| `theme` | chaîne | le défaut de l'éditeur (`turbo-classic`) | Nom du thème de couleurs de démarrage, tel que listé par `turbo-golo -list-themes` | | ||
| 22 | +| `autosave` | booléen | `false` | Si les fichiers modifiés sont écrits sans qu'on le demande. Le fichier qu'écrit **Create project settings** le met à `true` ; la valeur par défaut ici est celle qui s'applique à un projet sans fichier de réglages du tout. | | ||
| 23 | +| `autosave_delay` | chaîne | `"2s"` | Combien de temps attendre après la dernière frappe. Une durée Go : `"500ms"`, `"2s"`, `"1m"`. Consultée seulement quand `autosave` vaut true. | | ||
| 24 | + | ||
| 25 | +### Exemple | ||
| 26 | + | ||
| 27 | +```toml | ||
| 28 | +[editor] | ||
| 29 | +theme = "turbo-dark" | ||
| 30 | +autosave = true | ||
| 31 | +autosave_delay = "500ms" | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +## Priorité du thème | ||
| 35 | + | ||
| 36 | +De la plus forte à la plus faible : | ||
| 37 | + | ||
| 38 | +| Source | L'emporte sur | | ||
| 39 | +| --- | --- | | ||
| 40 | +| `-theme` sur la ligne de commande | tout | | ||
| 41 | +| `theme` dans le fichier de réglages | le défaut intégré | | ||
| 42 | +| Le défaut intégré `turbo-classic` | — | | ||
| 43 | + | ||
| 44 | +Un nom de thème inconnu, à n'importe quel niveau, retombe sur le défaut intégré plutôt que d'échouer. | ||
| 45 | + | ||
| 46 | +## Quand un changement prend effet | ||
| 47 | + | ||
| 48 | +Le fichier est lu au démarrage, et **de nouveau à chaque enregistrement depuis l'éditeur** — un changement fait dans l'éditeur est donc en vigueur dès que vous appuyez sur `F2`, sans redémarrage. | ||
| 49 | + | ||
| 50 | +| Clé | Ré-appliquée à l'enregistrement | Pourquoi | | ||
| 51 | +| --- | --- | --- | | ||
| 52 | +| `autosave` | oui | | | ||
| 53 | +| `autosave_delay` | oui | | | ||
| 54 | +| `theme` | **non** | Options ▸ Theme est la façon vivante de le changer, et y réécrit déjà le choix. Une option `-theme` donnée en ligne de commande est l'affirmation la plus explicite pour cette session, et l'enregistrement d'un fichier ne la contredit pas. | | ||
| 55 | + | ||
| 56 | +| Résultat | Barre d'état | | ||
| 57 | +| --- | --- | | ||
| 58 | +| Lu et appliqué | `Applied .turbo-golo/settings.toml — autosave on (2s)` | | ||
| 59 | +| Lu et appliqué, autosave désactivée | `Applied .turbo-golo/settings.toml — autosave off` | | ||
| 60 | +| Enregistré, mais plus du TOML valide | `Saved, but not applied: …` — les valeurs précédentes restent en vigueur | | ||
| 61 | + | ||
| 62 | +Enregistrer est enregistrer, quel qu'en soit l'auteur : la sauvegarde automatique qui écrit le fichier de réglages les ré-applique exactement comme `F2`. Une modification faite **hors** de l'éditeur n'est pas remarquée ; rien ne surveille le fichier. | ||
| 63 | + | ||
| 64 | +## Sauvegarde automatique | ||
| 65 | + | ||
| 66 | +| Comportement | Détail | | ||
| 67 | +| --- | --- | | ||
| 68 | +| Déclencheur | Le délai écoulé sans modification dans aucune fenêtre | | ||
| 69 | +| Portée | Tout fichier ouvert qui a un nom, pas seulement celui au premier plan | | ||
| 70 | +| Échéance | Une seule pour tout l'éditeur, relancée par toute modification dans n'importe quelle fenêtre | | ||
| 71 | +| Fichiers sans nom | Jamais enregistrés ; jamais l'objet d'une question | | ||
| 72 | +| Signalement | `Saved <nom>` dans la barre d'état | | ||
| 73 | +| Échec | Signalé dans la barre d'état, jamais dans un dialogue, et non réessayé avant la modification suivante | | ||
| 74 | +| Fermeture d'une fenêtre | Enregistre au lieu de demander, si le fichier a un nom | | ||
| 75 | +| Sortie de l'éditeur | Enregistre au lieu de demander, si le fichier a un nom | | ||
| 76 | + | ||
| 77 | +## Écritures | ||
| 78 | + | ||
| 79 | +Le fichier de réglages est écrit par exactement deux actions. Rien d'autre dans l'éditeur n'y écrit, et rien ne le crée tout seul. | ||
| 80 | + | ||
| 81 | +| Action | Effet | | ||
| 82 | +| --- | --- | | ||
| 83 | +| **Options ▸ Create project settings** | Crée `.turbo-golo/settings.toml` avec le thème en cours, `autosave = true` et des commentaires explicatifs. Grisée dès que le projet en a un, elle ne peut donc pas être choisie deux fois. | | ||
| 84 | +| **Options ▸ Theme** | Réécrit la valeur de `theme` **uniquement si le fichier existe déjà**. Commentaires, lignes vides, ordre des clés et commentaire de fin de la ligne du thème sont conservés. | | ||
| 85 | + | ||
| 86 | +Les deux écrivent via un fichier temporaire du même dossier, renommé en place : une écriture interrompue laisse le fichier précédent intact. | ||
| 87 | + | ||
| 88 | +## Entrées de menu | ||
| 89 | + | ||
| 90 | +| Entrée | Menu | Fichier requis | Effet | | ||
| 91 | +| --- | --- | --- | --- | | ||
| 92 | +| Create project settings | Options | refuse le fichier | Comme ci-dessus, puis ouvre le fichier. Grisée dès que le projet en a un. | | ||
| 93 | +| Project settings… | Options | exige le fichier | Ouvre `.turbo-golo/settings.toml`. Grisée tant que le projet n'en a pas. | | ||
| 94 | + | ||
| 95 | +## Erreurs | ||
| 96 | + | ||
| 97 | +| Message | Cause | | ||
| 98 | +| --- | --- | | ||
| 99 | +| `turbo-golo: reading …/settings.toml: …` sur la sortie d'erreur | Le fichier est là mais n'est pas du TOML valide. L'éditeur s'ouvre avec ses valeurs par défaut. | | ||
| 100 | +| `reading …: autosave_delay "x" is not a duration such as "2s"` | `autosave_delay` n'est pas une durée Go | | ||
| 101 | +| `reading …: autosave_delay must be positive, not "0s"` | `autosave_delay` est nul ou négatif | | ||
| 102 | +| `Already there: .turbo-golo/settings.toml` dans la barre d'état | Créer dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; reste possible pour un appelant qui n'est pas un menu. | | ||
| 103 | +| `Saved, but not applied: …` dans la barre d'état | Le fichier de réglages a été écrit mais ne s'analyse plus. Les valeurs précédentes restent en vigueur. | | ||
| 104 | +| `This project has no .turbo-golo/settings.toml yet.` | **Project settings…** dans un projet qui n'en a pas | | ||
| 105 | +| `Theme set for this session only: …` | Le thème a changé mais le fichier de réglages n'a pas pu être écrit | | ||
| 106 | + | ||
| 107 | +## Voir aussi | ||
| 108 | + | ||
| 109 | +- [Donner ses propres réglages à un projet](../how-to/configure-a-project.md) | ||
| 110 | +- [Réglages de projet](../explanation/project-settings.md) | ||
| 111 | +- [Format des fichiers de thème](themes.md) — un autre fichier, dans le même langage | ||
added
docs/fr/reference/project-tree.md +102 -0 | new file mode 100644 | ||
| @@ -0,0 +1,102 @@ | ||
| 1 | +# Référence : arbre du projet | |
| 2 | + | |
| 3 | +> Description neutre de la fenêtre d'arbre du projet : ce qu'elle montre, ce qu'elle cache, et les touches auxquelles elle répond. | |
| 4 | + | |
| 5 | +## Ouverture | |
| 6 | + | |
| 7 | +| Chemin | Condition | | |
| 8 | +| --- | --- | | |
| 9 | +| `F9` | Toujours | | |
| 10 | +| **Window ▸ Project tree** | Toujours | | |
| 11 | + | |
| 12 | +Aucun des deux n'exige qu'un fichier soit ouvert. Les deux ramènent l'arbre existant au premier plan quand il y en a déjà un : il y a au plus une fenêtre d'arbre. | |
| 13 | + | |
| 14 | +## Racine | |
| 15 | + | |
| 16 | +| Propriété | Valeur | | |
| 17 | +| --- | --- | | |
| 18 | +| Enracinée à | Le dossier depuis lequel l'éditeur a été lancé (`os.Getwd()`) | | |
| 19 | +| Recherche | Ce dossier seulement. Les dossiers parents ne sont **pas** parcourus, la même règle que `.turbo-golo/settings.toml`. | | |
| 20 | +| Titre de la fenêtre | Le nom de base de ce dossier | | |
| 21 | +| Ligne de la racine | Non affichée ; la première ligne est la première entrée du projet | | |
| 22 | + | |
| 23 | +## Ce qui est listé | |
| 24 | + | |
| 25 | +| Règle | Détail | | |
| 26 | +| --- | --- | | |
| 27 | +| Ordre | Les dossiers d'abord, puis les fichiers ; chaque groupe trié par nom | | |
| 28 | +| Masqué | `.git` seulement | | |
| 29 | +| Affiché | Toute autre entrée, y compris celles commençant par un point — `.turbo-golo`, `.gitignore`, `.qlty` | | |
| 30 | +| Lecture | Un dossier est lu la première fois qu'il est déplié, pas avant | | |
| 31 | +| Dossier illisible | Apparaît déplié et vide ; le reste de l'arbre n'est pas affecté | | |
| 32 | + | |
| 33 | +## Marqueurs | |
| 34 | + | |
| 35 | +| Marqueur | Signification | | |
| 36 | +| --- | --- | | |
| 37 | +| `▶ ` | Un dossier fermé | | |
| 38 | +| `▼ ` | Un dossier ouvert | | |
| 39 | +| (deux espaces) | Un fichier — indenté de la largeur d'un marqueur, pour que les noms s'alignent | | |
| 40 | + | |
| 41 | +Chaque niveau de profondeur ajoute deux espaces d'indentation. | |
| 42 | + | |
| 43 | +## Touches | |
| 44 | + | |
| 45 | +Traitées quand la fenêtre de l'arbre a le focus. | |
| 46 | + | |
| 47 | +| Touche | Action | | |
| 48 | +| --- | --- | | |
| 49 | +| `↑` `↓` | Ligne précédente / suivante | | |
| 50 | +| `Page↑` `Page↓` | Un écran à la fois | | |
| 51 | +| `Début` `Fin` | Première / dernière ligne | | |
| 52 | +| `→` | Déplier un dossier fermé ; sinon aller à la ligne suivante | | |
| 53 | +| `←` | Replier un dossier ouvert ; sinon remonter au dossier qui contient cette ligne | | |
| 54 | +| `Entrée` | Ouvrir un fichier ; déplier ou replier un dossier | | |
| 55 | +| `F5`, `Ctrl-R` | Relire le projet | | |
| 56 | + | |
| 57 | +Les raccourcis de l'éditeur s'appliquent comme d'habitude : `F6` passe à la fenêtre suivante, `Ctrl-W` ferme l'arbre, `Alt-X` quitte. | |
| 58 | + | |
| 59 | +## Souris | |
| 60 | + | |
| 61 | +| Action | Effet | | |
| 62 | +| --- | --- | | |
| 63 | +| Clic sur une ligne | Y déplacer la surbrillance | | |
| 64 | +| Clic sur la ligne surlignée | Agir dessus, comme `Entrée` | | |
| 65 | +| Molette haut / bas | Déplacer la surbrillance de trois lignes | | |
| 66 | + | |
| 67 | +## Rafraîchissement | |
| 68 | + | |
| 69 | +| Déclencheur | Effet | | |
| 70 | +| --- | --- | | |
| 71 | +| `F5` ou `Ctrl-R` | Relit tous les dossiers qui ont été ouverts | | |
| 72 | +| Enregistrer un fichier | La même chose, automatiquement | | |
| 73 | +| Déplier un dossier | Lit ce dossier, s'il n'a pas encore été lu | | |
| 74 | + | |
| 75 | +Le rafraîchissement conserve la forme de l'arbre : un dossier ouvert le reste, un dossier supprimé emporte sa branche, et les dossiers que personne n'a ouverts restent non lus. La surbrillance reste sur la même entrée, ou sur la ligne restante la plus proche si cette entrée a disparu. | |
| 76 | + | |
| 77 | +L'arbre ne surveille **pas** le système de fichiers. Un fichier créé par une fenêtre terminal — un `golo new`, un `gogolo build` — ou par un `git checkout` n'apparaît qu'après un rafraîchissement. | |
| 78 | + | |
| 79 | +## Couleurs | |
| 80 | + | |
| 81 | +| Clé de thème | Ce qu'elle colore | | |
| 82 | +| --- | --- | | |
| 83 | +| `tree.text` | Le nom d'un fichier dans l'arbre, et le fond de l'arbre | | |
| 84 | +| `tree.directory` | Le nom d'un dossier | | |
| 85 | +| `tree.selected` | La ligne surlignée, quand l'arbre a le focus | | |
| 86 | +| `tree.unfocused` | La ligne surlignée, quand il ne l'a pas | | |
| 87 | + | |
| 88 | +Elles ne se rabattent pas sur les clés `list.*` : le rabattement suit les points et s'arrête à `default`. Voir [Format des fichiers de thème](themes.md). | |
| 89 | + | |
| 90 | +## Erreurs | |
| 91 | + | |
| 92 | +| Message | Cause | | |
| 93 | +| --- | --- | | |
| 94 | +| `Cannot tell which directory this is: …` | Le répertoire de travail n'a pas pu être lu | | |
| 95 | +| `reading …: …` | Le dossier du projet n'a pas pu être lu | | |
| 96 | +| `… is not a directory` | La racine désigne un fichier | | |
| 97 | + | |
| 98 | +## Voir aussi | |
| 99 | + | |
| 100 | +- [Parcourir un projet et ouvrir des fichiers depuis un arbre](../how-to/browse-a-project.md) | |
| 101 | +- [Arbre du projet](../explanation/project-tree.md) | |
| 102 | +- [Clavier](keyboard.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,102 @@ | |||
| 1 | +# Référence : arbre du projet | ||
| 2 | + | ||
| 3 | +> Description neutre de la fenêtre d'arbre du projet : ce qu'elle montre, ce qu'elle cache, et les touches auxquelles elle répond. | ||
| 4 | + | ||
| 5 | +## Ouverture | ||
| 6 | + | ||
| 7 | +| Chemin | Condition | | ||
| 8 | +| --- | --- | | ||
| 9 | +| `F9` | Toujours | | ||
| 10 | +| **Window ▸ Project tree** | Toujours | | ||
| 11 | + | ||
| 12 | +Aucun des deux n'exige qu'un fichier soit ouvert. Les deux ramènent l'arbre existant au premier plan quand il y en a déjà un : il y a au plus une fenêtre d'arbre. | ||
| 13 | + | ||
| 14 | +## Racine | ||
| 15 | + | ||
| 16 | +| Propriété | Valeur | | ||
| 17 | +| --- | --- | | ||
| 18 | +| Enracinée à | Le dossier depuis lequel l'éditeur a été lancé (`os.Getwd()`) | | ||
| 19 | +| Recherche | Ce dossier seulement. Les dossiers parents ne sont **pas** parcourus, la même règle que `.turbo-golo/settings.toml`. | | ||
| 20 | +| Titre de la fenêtre | Le nom de base de ce dossier | | ||
| 21 | +| Ligne de la racine | Non affichée ; la première ligne est la première entrée du projet | | ||
| 22 | + | ||
| 23 | +## Ce qui est listé | ||
| 24 | + | ||
| 25 | +| Règle | Détail | | ||
| 26 | +| --- | --- | | ||
| 27 | +| Ordre | Les dossiers d'abord, puis les fichiers ; chaque groupe trié par nom | | ||
| 28 | +| Masqué | `.git` seulement | | ||
| 29 | +| Affiché | Toute autre entrée, y compris celles commençant par un point — `.turbo-golo`, `.gitignore`, `.qlty` | | ||
| 30 | +| Lecture | Un dossier est lu la première fois qu'il est déplié, pas avant | | ||
| 31 | +| Dossier illisible | Apparaît déplié et vide ; le reste de l'arbre n'est pas affecté | | ||
| 32 | + | ||
| 33 | +## Marqueurs | ||
| 34 | + | ||
| 35 | +| Marqueur | Signification | | ||
| 36 | +| --- | --- | | ||
| 37 | +| `▶ ` | Un dossier fermé | | ||
| 38 | +| `▼ ` | Un dossier ouvert | | ||
| 39 | +| (deux espaces) | Un fichier — indenté de la largeur d'un marqueur, pour que les noms s'alignent | | ||
| 40 | + | ||
| 41 | +Chaque niveau de profondeur ajoute deux espaces d'indentation. | ||
| 42 | + | ||
| 43 | +## Touches | ||
| 44 | + | ||
| 45 | +Traitées quand la fenêtre de l'arbre a le focus. | ||
| 46 | + | ||
| 47 | +| Touche | Action | | ||
| 48 | +| --- | --- | | ||
| 49 | +| `↑` `↓` | Ligne précédente / suivante | | ||
| 50 | +| `Page↑` `Page↓` | Un écran à la fois | | ||
| 51 | +| `Début` `Fin` | Première / dernière ligne | | ||
| 52 | +| `→` | Déplier un dossier fermé ; sinon aller à la ligne suivante | | ||
| 53 | +| `←` | Replier un dossier ouvert ; sinon remonter au dossier qui contient cette ligne | | ||
| 54 | +| `Entrée` | Ouvrir un fichier ; déplier ou replier un dossier | | ||
| 55 | +| `F5`, `Ctrl-R` | Relire le projet | | ||
| 56 | + | ||
| 57 | +Les raccourcis de l'éditeur s'appliquent comme d'habitude : `F6` passe à la fenêtre suivante, `Ctrl-W` ferme l'arbre, `Alt-X` quitte. | ||
| 58 | + | ||
| 59 | +## Souris | ||
| 60 | + | ||
| 61 | +| Action | Effet | | ||
| 62 | +| --- | --- | | ||
| 63 | +| Clic sur une ligne | Y déplacer la surbrillance | | ||
| 64 | +| Clic sur la ligne surlignée | Agir dessus, comme `Entrée` | | ||
| 65 | +| Molette haut / bas | Déplacer la surbrillance de trois lignes | | ||
| 66 | + | ||
| 67 | +## Rafraîchissement | ||
| 68 | + | ||
| 69 | +| Déclencheur | Effet | | ||
| 70 | +| --- | --- | | ||
| 71 | +| `F5` ou `Ctrl-R` | Relit tous les dossiers qui ont été ouverts | | ||
| 72 | +| Enregistrer un fichier | La même chose, automatiquement | | ||
| 73 | +| Déplier un dossier | Lit ce dossier, s'il n'a pas encore été lu | | ||
| 74 | + | ||
| 75 | +Le rafraîchissement conserve la forme de l'arbre : un dossier ouvert le reste, un dossier supprimé emporte sa branche, et les dossiers que personne n'a ouverts restent non lus. La surbrillance reste sur la même entrée, ou sur la ligne restante la plus proche si cette entrée a disparu. | ||
| 76 | + | ||
| 77 | +L'arbre ne surveille **pas** le système de fichiers. Un fichier créé par une fenêtre terminal — un `golo new`, un `gogolo build` — ou par un `git checkout` n'apparaît qu'après un rafraîchissement. | ||
| 78 | + | ||
| 79 | +## Couleurs | ||
| 80 | + | ||
| 81 | +| Clé de thème | Ce qu'elle colore | | ||
| 82 | +| --- | --- | | ||
| 83 | +| `tree.text` | Le nom d'un fichier dans l'arbre, et le fond de l'arbre | | ||
| 84 | +| `tree.directory` | Le nom d'un dossier | | ||
| 85 | +| `tree.selected` | La ligne surlignée, quand l'arbre a le focus | | ||
| 86 | +| `tree.unfocused` | La ligne surlignée, quand il ne l'a pas | | ||
| 87 | + | ||
| 88 | +Elles ne se rabattent pas sur les clés `list.*` : le rabattement suit les points et s'arrête à `default`. Voir [Format des fichiers de thème](themes.md). | ||
| 89 | + | ||
| 90 | +## Erreurs | ||
| 91 | + | ||
| 92 | +| Message | Cause | | ||
| 93 | +| --- | --- | | ||
| 94 | +| `Cannot tell which directory this is: …` | Le répertoire de travail n'a pas pu être lu | | ||
| 95 | +| `reading …: …` | Le dossier du projet n'a pas pu être lu | | ||
| 96 | +| `… is not a directory` | La racine désigne un fichier | | ||
| 97 | + | ||
| 98 | +## Voir aussi | ||
| 99 | + | ||
| 100 | +- [Parcourir un projet et ouvrir des fichiers depuis un arbre](../how-to/browse-a-project.md) | ||
| 101 | +- [Arbre du projet](../explanation/project-tree.md) | ||
| 102 | +- [Clavier](keyboard.md) | ||
added
docs/fr/reference/snippets.md +128 -0 | new file mode 100644 | ||
| @@ -0,0 +1,128 @@ | ||
| 1 | +# Référence : snippets | |
| 2 | + | |
| 3 | +> Description neutre des fichiers de snippets, du menu Snippets, et de la façon dont un snippet est inséré. | |
| 4 | + | |
| 5 | +## Fichiers | |
| 6 | + | |
| 7 | +Les deux sont lus, et les deux sont facultatifs. | |
| 8 | + | |
| 9 | +| Fichier | Contient | | |
| 10 | +| --- | --- | | |
| 11 | +| `./.turbo-golo/snippets.toml` | Les snippets du projet | | |
| 12 | +| `$TURBO_GOLO_SNIPPET_DIR/snippets.toml`, sinon `<config utilisateur>/turbo-golo/snippets.toml` | Les vôtres, partagés entre projets | | |
| 13 | + | |
| 14 | +`<config utilisateur>` est `os.UserConfigDir()` : `~/.config` sous Linux, `~/Library/Application Support` sous macOS. `TURBO_GOLO_DIR` remplace `<config utilisateur>/turbo-golo` en entier. | |
| 15 | + | |
| 16 | +| Propriété | Valeur | | |
| 17 | +| --- | --- | | |
| 18 | +| Recherche du projet | Le répertoire de travail seulement. Les dossiers parents ne sont **pas** parcourus. | | |
| 19 | +| Lecture | À chaque ouverture du menu Snippets | | |
| 20 | +| Ordre | Les vôtres d'abord, puis ceux du projet | | |
| 21 | +| Conflit de nom | Même `group` **et** même `name` → celui du projet remplace le vôtre | | |
| 22 | +| Fichier absent | Pas une erreur | | |
| 23 | +| Fichier illisible | Une erreur, signalée dans le menu | | |
| 24 | + | |
| 25 | +## Format du fichier | |
| 26 | + | |
| 27 | +Une table `[[snippet]]` par snippet. | |
| 28 | + | |
| 29 | +| Clé | Type | Obligatoire | Description | | |
| 30 | +| --- | --- | --- | --- | | |
| 31 | +| `name` | chaîne | oui | Ce que le menu affiche | | |
| 32 | +| `body` | chaîne | oui | Le texte inséré au curseur | | |
| 33 | +| `group` | chaîne | non | Le sous-menu où il va ; absent signifie `General` | | |
| 34 | +| `languages` | tableau de chaînes | non | Restreint le snippet à ces langages ; absent signifie tous les fichiers | | |
| 35 | + | |
| 36 | +`languages` emploie les noms de langages de l'éditeur : `golo`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash`. Voir [Langages colorés](languages.md). | |
| 37 | + | |
| 38 | +Un snippet sans `name` ou sans `body` rend tout le fichier erroné — il n'aurait pu être affiché, ou n'aurait rien à insérer. | |
| 39 | + | |
| 40 | +### Exemple | |
| 41 | + | |
| 42 | +```toml | |
| 43 | +[[snippet]] | |
| 44 | +name = "function" | |
| 45 | +group = "Golo" | |
| 46 | +languages = ["golo"] | |
| 47 | +body = ''' | |
| 48 | +function name = |a, b| { | |
| 49 | + return a + b | |
| 50 | +}''' | |
| 51 | +``` | |
| 52 | + | |
| 53 | +Les chaînes multi-lignes du TOML — `'''…'''` comme `"""…"""` — suppriment le saut de ligne qui suit immédiatement les guillemets d'ouverture. Dans une chaîne **basique** (`"""`), `\t` devient une tabulation et `\"` un guillemet à la lecture du fichier ; dans une chaîne **littérale** (`'''`), rien n'est interprété. Les corps Golo du fichier de départ sont des chaînes littérales, parce qu'une chaîne Golo porte `\n` et `\"` et doit arriver intacte dans le fichier. | |
| 54 | + | |
| 55 | +## Le fichier de départ | |
| 56 | + | |
| 57 | +**Create snippets file** écrit un fichier commenté contenant : | |
| 58 | + | |
| 59 | +| Groupe | `languages` | Snippets | | |
| 60 | +| --- | --- | --- | | |
| 61 | +| Golo | `["golo"]` | `module`, `main`, `function`, `closure`, `struct`, `union`, `augment`, `match`, `foreach`, `for`, `try`, `comprehension` | | |
| 62 | +| General | aucun | `Hello` | | |
| 63 | +| Markdown | `["markdown"]` | `Image` | | |
| 64 | + | |
| 65 | +Les corps Golo sont indentés de deux espaces, la convention de tous les exemples de GoloScript ; Golo n'a pas de formateur qui en imposerait une autre. | |
| 66 | + | |
| 67 | +## Le menu | |
| 68 | + | |
| 69 | +| Entrée | Condition | | |
| 70 | +| --- | --- | | |
| 71 | +| Un sous-menu par groupe, dans l'ordre d'apparition des groupes dans les fichiers | Un groupe ayant au moins un snippet applicable à la fenêtre au premier plan | | |
| 72 | +| `Cannot read snippets`, grisé | Un fichier est présent mais illisible | | |
| 73 | +| `Create snippets file` | Le projet n'a pas de fichier de snippets | | |
| 74 | +| `Open snippets file` | Le projet en a un | | |
| 75 | + | |
| 76 | +La touche d'accès du menu est `Alt-N`, pas `Alt-S` : Search répond déjà au S. | |
| 77 | + | |
| 78 | +Les groupes, et les snippets à l'intérieur, sortent dans l'ordre de lecture : le menu correspond aux fichiers. | |
| 79 | + | |
| 80 | +Une entrée de snippet est grisée quand aucun fichier n'est ouvert pour l'y insérer — un terminal ou l'arbre du projet au premier plan compte comme aucun fichier. | |
| 81 | + | |
| 82 | +### Filtrage | |
| 83 | + | |
| 84 | +| Fenêtre au premier plan | Snippets proposés | | |
| 85 | +| --- | --- | | |
| 86 | +| Un fichier d'un langage reconnu | Ceux qui nomment ce langage, plus ceux qui n'en nomment aucun | | |
| 87 | +| Un fichier d'aucun langage reconnu | Ceux qui n'en nomment aucun | | |
| 88 | +| Un terminal, l'arbre du projet, ou rien | Ceux qui n'en nomment aucun | | |
| 89 | + | |
| 90 | +Un fichier `.golo`, ou un fichier sans extension dont la première ligne est un shebang nommant `golo`, est du langage `golo`. | |
| 91 | + | |
| 92 | +## Insertion | |
| 93 | + | |
| 94 | +| Comportement | Détail | | |
| 95 | +| --- | --- | | |
| 96 | +| Position | Au curseur | | |
| 97 | +| Première ligne | Insérée là où est le curseur | | |
| 98 | +| Lignes suivantes | Préfixées par l'indentation de la ligne où était le curseur | | |
| 99 | +| Lignes vides du corps | Laissées vides, non complétées d'espaces | | |
| 100 | +| Annulation | Une seule opération pour tout le snippet | | |
| 101 | +| Curseur ensuite | À la fin du texte inséré | | |
| 102 | +| Signalement | `Snippet inserted` dans la barre d'état | | |
| 103 | + | |
| 104 | +L'indentation copiée est le **préfixe d'espaces de la ligne courante**, tabulations ou espaces telles quelles : un snippet suit donc ce que le fichier emploie déjà. | |
| 105 | + | |
| 106 | +## Entrées de menu | |
| 107 | + | |
| 108 | +| Entrée | Menu | Effet | | |
| 109 | +| --- | --- | --- | | |
| 110 | +| Create snippets file | Snippets | Écrit `.turbo-golo/snippets.toml` avec les exemples ci-dessus, puis l'ouvre. Grisée dès que le projet en a un. | | |
| 111 | +| Open snippets file | Snippets | Ouvre `.turbo-golo/snippets.toml`. Grisée tant que le projet n'en a pas. Toujours le fichier du projet, jamais le vôtre — c'est celui qu'écrit l'entrée au-dessus. | | |
| 112 | + | |
| 113 | +Le fichier est écrit via un fichier temporaire du même dossier, renommé en place : une écriture interrompue laisse le fichier précédent intact. | |
| 114 | + | |
| 115 | +## Erreurs | |
| 116 | + | |
| 117 | +| Message | Cause | | |
| 118 | +| --- | --- | | |
| 119 | +| `Cannot read snippets` dans le menu | Un fichier de snippets est présent mais n'est pas du TOML valide, ou contient un snippet sans nom ou sans corps | | |
| 120 | +| `Already there: .turbo-golo/snippets.toml` | Créer dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; reste possible pour un appelant qui n'est pas un menu. | | |
| 121 | +| `This project has no .turbo-golo/snippets.toml yet.` | Ouvrir dans un projet qui n'en a pas, de même | | |
| 122 | +| `Cannot tell which directory this is: …` | Le répertoire de travail n'a pas pu être lu | | |
| 123 | + | |
| 124 | +## Voir aussi | |
| 125 | + | |
| 126 | +- [Insérer des snippets depuis un menu](../how-to/use-snippets.md) | |
| 127 | +- [Snippets](../explanation/snippets.md) | |
| 128 | +- [Clavier](keyboard.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,128 @@ | |||
| 1 | +# Référence : snippets | ||
| 2 | + | ||
| 3 | +> Description neutre des fichiers de snippets, du menu Snippets, et de la façon dont un snippet est inséré. | ||
| 4 | + | ||
| 5 | +## Fichiers | ||
| 6 | + | ||
| 7 | +Les deux sont lus, et les deux sont facultatifs. | ||
| 8 | + | ||
| 9 | +| Fichier | Contient | | ||
| 10 | +| --- | --- | | ||
| 11 | +| `./.turbo-golo/snippets.toml` | Les snippets du projet | | ||
| 12 | +| `$TURBO_GOLO_SNIPPET_DIR/snippets.toml`, sinon `<config utilisateur>/turbo-golo/snippets.toml` | Les vôtres, partagés entre projets | | ||
| 13 | + | ||
| 14 | +`<config utilisateur>` est `os.UserConfigDir()` : `~/.config` sous Linux, `~/Library/Application Support` sous macOS. `TURBO_GOLO_DIR` remplace `<config utilisateur>/turbo-golo` en entier. | ||
| 15 | + | ||
| 16 | +| Propriété | Valeur | | ||
| 17 | +| --- | --- | | ||
| 18 | +| Recherche du projet | Le répertoire de travail seulement. Les dossiers parents ne sont **pas** parcourus. | | ||
| 19 | +| Lecture | À chaque ouverture du menu Snippets | | ||
| 20 | +| Ordre | Les vôtres d'abord, puis ceux du projet | | ||
| 21 | +| Conflit de nom | Même `group` **et** même `name` → celui du projet remplace le vôtre | | ||
| 22 | +| Fichier absent | Pas une erreur | | ||
| 23 | +| Fichier illisible | Une erreur, signalée dans le menu | | ||
| 24 | + | ||
| 25 | +## Format du fichier | ||
| 26 | + | ||
| 27 | +Une table `[[snippet]]` par snippet. | ||
| 28 | + | ||
| 29 | +| Clé | Type | Obligatoire | Description | | ||
| 30 | +| --- | --- | --- | --- | | ||
| 31 | +| `name` | chaîne | oui | Ce que le menu affiche | | ||
| 32 | +| `body` | chaîne | oui | Le texte inséré au curseur | | ||
| 33 | +| `group` | chaîne | non | Le sous-menu où il va ; absent signifie `General` | | ||
| 34 | +| `languages` | tableau de chaînes | non | Restreint le snippet à ces langages ; absent signifie tous les fichiers | | ||
| 35 | + | ||
| 36 | +`languages` emploie les noms de langages de l'éditeur : `golo`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash`. Voir [Langages colorés](languages.md). | ||
| 37 | + | ||
| 38 | +Un snippet sans `name` ou sans `body` rend tout le fichier erroné — il n'aurait pu être affiché, ou n'aurait rien à insérer. | ||
| 39 | + | ||
| 40 | +### Exemple | ||
| 41 | + | ||
| 42 | +```toml | ||
| 43 | +[[snippet]] | ||
| 44 | +name = "function" | ||
| 45 | +group = "Golo" | ||
| 46 | +languages = ["golo"] | ||
| 47 | +body = ''' | ||
| 48 | +function name = |a, b| { | ||
| 49 | + return a + b | ||
| 50 | +}''' | ||
| 51 | +``` | ||
| 52 | + | ||
| 53 | +Les chaînes multi-lignes du TOML — `'''…'''` comme `"""…"""` — suppriment le saut de ligne qui suit immédiatement les guillemets d'ouverture. Dans une chaîne **basique** (`"""`), `\t` devient une tabulation et `\"` un guillemet à la lecture du fichier ; dans une chaîne **littérale** (`'''`), rien n'est interprété. Les corps Golo du fichier de départ sont des chaînes littérales, parce qu'une chaîne Golo porte `\n` et `\"` et doit arriver intacte dans le fichier. | ||
| 54 | + | ||
| 55 | +## Le fichier de départ | ||
| 56 | + | ||
| 57 | +**Create snippets file** écrit un fichier commenté contenant : | ||
| 58 | + | ||
| 59 | +| Groupe | `languages` | Snippets | | ||
| 60 | +| --- | --- | --- | | ||
| 61 | +| Golo | `["golo"]` | `module`, `main`, `function`, `closure`, `struct`, `union`, `augment`, `match`, `foreach`, `for`, `try`, `comprehension` | | ||
| 62 | +| General | aucun | `Hello` | | ||
| 63 | +| Markdown | `["markdown"]` | `Image` | | ||
| 64 | + | ||
| 65 | +Les corps Golo sont indentés de deux espaces, la convention de tous les exemples de GoloScript ; Golo n'a pas de formateur qui en imposerait une autre. | ||
| 66 | + | ||
| 67 | +## Le menu | ||
| 68 | + | ||
| 69 | +| Entrée | Condition | | ||
| 70 | +| --- | --- | | ||
| 71 | +| Un sous-menu par groupe, dans l'ordre d'apparition des groupes dans les fichiers | Un groupe ayant au moins un snippet applicable à la fenêtre au premier plan | | ||
| 72 | +| `Cannot read snippets`, grisé | Un fichier est présent mais illisible | | ||
| 73 | +| `Create snippets file` | Le projet n'a pas de fichier de snippets | | ||
| 74 | +| `Open snippets file` | Le projet en a un | | ||
| 75 | + | ||
| 76 | +La touche d'accès du menu est `Alt-N`, pas `Alt-S` : Search répond déjà au S. | ||
| 77 | + | ||
| 78 | +Les groupes, et les snippets à l'intérieur, sortent dans l'ordre de lecture : le menu correspond aux fichiers. | ||
| 79 | + | ||
| 80 | +Une entrée de snippet est grisée quand aucun fichier n'est ouvert pour l'y insérer — un terminal ou l'arbre du projet au premier plan compte comme aucun fichier. | ||
| 81 | + | ||
| 82 | +### Filtrage | ||
| 83 | + | ||
| 84 | +| Fenêtre au premier plan | Snippets proposés | | ||
| 85 | +| --- | --- | | ||
| 86 | +| Un fichier d'un langage reconnu | Ceux qui nomment ce langage, plus ceux qui n'en nomment aucun | | ||
| 87 | +| Un fichier d'aucun langage reconnu | Ceux qui n'en nomment aucun | | ||
| 88 | +| Un terminal, l'arbre du projet, ou rien | Ceux qui n'en nomment aucun | | ||
| 89 | + | ||
| 90 | +Un fichier `.golo`, ou un fichier sans extension dont la première ligne est un shebang nommant `golo`, est du langage `golo`. | ||
| 91 | + | ||
| 92 | +## Insertion | ||
| 93 | + | ||
| 94 | +| Comportement | Détail | | ||
| 95 | +| --- | --- | | ||
| 96 | +| Position | Au curseur | | ||
| 97 | +| Première ligne | Insérée là où est le curseur | | ||
| 98 | +| Lignes suivantes | Préfixées par l'indentation de la ligne où était le curseur | | ||
| 99 | +| Lignes vides du corps | Laissées vides, non complétées d'espaces | | ||
| 100 | +| Annulation | Une seule opération pour tout le snippet | | ||
| 101 | +| Curseur ensuite | À la fin du texte inséré | | ||
| 102 | +| Signalement | `Snippet inserted` dans la barre d'état | | ||
| 103 | + | ||
| 104 | +L'indentation copiée est le **préfixe d'espaces de la ligne courante**, tabulations ou espaces telles quelles : un snippet suit donc ce que le fichier emploie déjà. | ||
| 105 | + | ||
| 106 | +## Entrées de menu | ||
| 107 | + | ||
| 108 | +| Entrée | Menu | Effet | | ||
| 109 | +| --- | --- | --- | | ||
| 110 | +| Create snippets file | Snippets | Écrit `.turbo-golo/snippets.toml` avec les exemples ci-dessus, puis l'ouvre. Grisée dès que le projet en a un. | | ||
| 111 | +| Open snippets file | Snippets | Ouvre `.turbo-golo/snippets.toml`. Grisée tant que le projet n'en a pas. Toujours le fichier du projet, jamais le vôtre — c'est celui qu'écrit l'entrée au-dessus. | | ||
| 112 | + | ||
| 113 | +Le fichier est écrit via un fichier temporaire du même dossier, renommé en place : une écriture interrompue laisse le fichier précédent intact. | ||
| 114 | + | ||
| 115 | +## Erreurs | ||
| 116 | + | ||
| 117 | +| Message | Cause | | ||
| 118 | +| --- | --- | | ||
| 119 | +| `Cannot read snippets` dans le menu | Un fichier de snippets est présent mais n'est pas du TOML valide, ou contient un snippet sans nom ou sans corps | | ||
| 120 | +| `Already there: .turbo-golo/snippets.toml` | Créer dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; reste possible pour un appelant qui n'est pas un menu. | | ||
| 121 | +| `This project has no .turbo-golo/snippets.toml yet.` | Ouvrir dans un projet qui n'en a pas, de même | | ||
| 122 | +| `Cannot tell which directory this is: …` | Le répertoire de travail n'a pas pu être lu | | ||
| 123 | + | ||
| 124 | +## Voir aussi | ||
| 125 | + | ||
| 126 | +- [Insérer des snippets depuis un menu](../how-to/use-snippets.md) | ||
| 127 | +- [Snippets](../explanation/snippets.md) | ||
| 128 | +- [Clavier](keyboard.md) | ||
added
docs/fr/reference/terminal.md +228 -0 | new file mode 100644 | ||
| @@ -0,0 +1,228 @@ | ||
| 1 | +# Référence : fenêtres terminal | |
| 2 | + | |
| 3 | +> Description neutre des fenêtres terminal ouvertes par Turbo Golo, des touches auxquelles elles répondent et des séquences d'échappement que l'émulateur implémente. | |
| 4 | + | |
| 5 | +## Ouverture | |
| 6 | + | |
| 7 | +| Chemin | Condition | | |
| 8 | +| --- | --- | | |
| 9 | +| `F8` | Toujours | | |
| 10 | +| **Window ▸ New terminal** | Toujours | | |
| 11 | + | |
| 12 | +Aucun des deux n'exige qu'un fichier soit ouvert. Les outils du menu **Golo** dont la sortie est `terminal` — le REPL, le débogueur, l'exécution d'un script — ouvrent eux aussi une fenêtre terminal ; voir [Outils Golo](golo-tools.md). | |
| 13 | + | |
| 14 | +## Le shell | |
| 15 | + | |
| 16 | +| Propriété | Valeur | | |
| 17 | +| --- | --- | | |
| 18 | +| Programme | `$SHELL`, ou `/bin/sh` si la variable est absente ou vide ; sous Windows `%COMSPEC%`, ou `cmd.exe` | | |
| 19 | +| Répertoire de travail | Le dossier du fichier de la fenêtre au premier plan ; le répertoire de travail de l'éditeur si aucun fichier n'est ouvert | | |
| 20 | +| `TERM` | `xterm-256color`, toujours — remplaçant toute valeur héritée | | |
| 21 | +| Environnement | Celui de l'éditeur, avec `TERM` remplacé | | |
| 22 | +| Terminal de contrôle | Oui : sous Linux et macOS le shell tourne dans sa propre session avec le pseudo-terminal comme terminal de contrôle ; sous Windows il est attaché à une pseudo-console. Dans les deux cas le contrôle de tâches et `Ctrl-C` fonctionnent | | |
| 23 | +| Taille initiale | Celle de la fenêtre, mise à jour à chaque redimensionnement | | |
| 24 | + | |
| 25 | +## Plateformes supportées | |
| 26 | + | |
| 27 | +| Plateforme | Comportement | | |
| 28 | +| --- | --- | | |
| 29 | +| Linux | Supportée (`/dev/ptmx`) | | |
| 30 | +| macOS | Supportée (`/dev/ptmx`) | | |
| 31 | +| Windows | Supportée (pseudo-console, ConPTY) : Windows 10 version 1809 ou plus récent. Compilé et vérifié ; **pas encore exécuté par les auteurs** sur une machine Windows | | |
| 32 | +| Autres | `F8` ouvre un message indiquant que les fenêtres terminal ne sont pas encore supportées ; rien d'autre ne change | | |
| 33 | + | |
| 34 | +## Touches | |
| 35 | + | |
| 36 | +### Après la fin du programme | |
| 37 | + | |
| 38 | +Une fenêtre dont la commande est terminée garde sa sortie mais cesse de se comporter comme un terminal : seules `Maj-Page↑` et `Maj-Page↓` sont encore prises, et toute autre touche atteint l'éditeur — c'est ce qui permet à `Ctrl-W` de la fermer. | |
| 39 | + | |
| 40 | +### Envoyées au shell | |
| 41 | + | |
| 42 | +Toute touche non listée ci-dessous sous « conservées par l'éditeur », encodée comme un terminal l'attend. | |
| 43 | + | |
| 44 | +| Touche | Octets envoyés | | |
| 45 | +| --- | --- | | |
| 46 | +| caractère imprimable | son encodage UTF-8 | | |
| 47 | +| `Alt-<touche>` | `ESC` suivi des octets de cette touche | | |
| 48 | +| `Ctrl-A` … `Ctrl-Z` | `0x01` … `0x1a` | | |
| 49 | +| `Enter` | `\r` | | |
| 50 | +| `Tab` | `\t` | | |
| 51 | +| `Shift-Tab` | `ESC [ Z` | | |
| 52 | +| `Backspace` | `0x7f` | | |
| 53 | +| `Escape` | `0x1b` | | |
| 54 | +| `↑` `↓` `→` `←` | `ESC [ A B C D`, ou `ESC O A B C D` en mode curseur application | | |
| 55 | +| `Home` `End` | `ESC [ H`, `ESC [ F`, ou les formes `ESC O` en mode curseur application | | |
| 56 | +| `Insert` `Delete` | `ESC [ 2~`, `ESC [ 3~` | | |
| 57 | +| `PgUp` `PgDn` | `ESC [ 5~`, `ESC [ 6~` | | |
| 58 | +| `F1` … `F4` | `ESC O P Q R S` | | |
| 59 | +| `F5` … `F12` | `ESC [ 15~ 17~ 18~ 19~ 20~ 21~ 23~ 24~` | | |
| 60 | + | |
| 61 | +Une touche sans signification pour un terminal n'envoie rien. | |
| 62 | + | |
| 63 | +### Conservées par l'éditeur | |
| 64 | + | |
| 65 | +| Touche | Action | | |
| 66 | +| --- | --- | | |
| 67 | +| `F1` … `F12` | Leur action habituelle dans l'éditeur | | |
| 68 | +| `Alt-X` | Quitter | | |
| 69 | +| `Alt-0` … `Alt-9` | Lister les fenêtres / passer la fenêtre 1…9 au premier plan | | |
| 70 | + | |
| 71 | +Les touches de fonction n'atteignent donc jamais un programme lancé dans une fenêtre terminal. | |
| 72 | + | |
| 73 | +### Traitées par la fenêtre terminal elle-même | |
| 74 | + | |
| 75 | +| Touche | Action | | |
| 76 | +| --- | --- | | |
| 77 | +| `Shift-PgUp` | Reculer d'un écran dans l'historique | | |
| 78 | +| `Shift-PgDn` | Avancer d'un écran | | |
| 79 | + | |
| 80 | +Toute touche envoyée au shell ramène également la vue à l'écran vivant. | |
| 81 | + | |
| 82 | +## Souris | |
| 83 | + | |
| 84 | +| Action | Effet | | |
| 85 | +| --- | --- | | |
| 86 | +| Molette haut / bas | Défiler de trois lignes dans l'historique | | |
| 87 | +| Clic | Passe la fenêtre au premier plan ; n'est pas transmis au programme | | |
| 88 | + | |
| 89 | +Le rapport souris n'est pas implémenté : un programme n'est jamais informé des clics. | |
| 90 | + | |
| 91 | +## Historique | |
| 92 | + | |
| 93 | +| Propriété | Valeur | | |
| 94 | +| --- | --- | | |
| 95 | +| Lignes conservées | 2000 | | |
| 96 | +| Ce qui est conservé | Uniquement les lignes sorties par le haut de l'écran principal | | |
| 97 | +| Écran alternatif | Non conservé — un programme plein écran ne laisse aucun historique | | |
| 98 | + | |
| 99 | +## Émulation | |
| 100 | + | |
| 101 | +`TERM` vaut `xterm-256color`. Voici ce qui en est implémenté. | |
| 102 | + | |
| 103 | +### Caractères de contrôle | |
| 104 | + | |
| 105 | +| Octet | Effet | | |
| 106 | +| --- | --- | | |
| 107 | +| `0x07` BEL | Noté ; l'éditeur ne l'émet pas | | |
| 108 | +| `0x08` BS | Curseur d'une colonne à gauche | | |
| 109 | +| `0x09` HT | Jusqu'à la tabulation suivante, toutes les 8 colonnes | | |
| 110 | +| `0x0a` `0x0b` `0x0c` | Saut de ligne | | |
| 111 | +| `0x0d` CR | Colonne 1 | | |
| 112 | + | |
| 113 | +### Séquences d'échappement | |
| 114 | + | |
| 115 | +| Séquence | Nom | Effet | | |
| 116 | +| --- | --- | --- | | |
| 117 | +| `ESC D` | IND | Saut de ligne | | |
| 118 | +| `ESC E` | NEL | Retour chariot et saut de ligne | | |
| 119 | +| `ESC M` | RI | Saut de ligne inverse, en conservant la colonne | | |
| 120 | +| `ESC 7` | DECSC | Sauvegarder le curseur et le style | | |
| 121 | +| `ESC 8` | DECRC | Les restaurer | | |
| 122 | +| `ESC c` | RIS | Réinitialisation complète | | |
| 123 | + | |
| 124 | +### Séquences CSI | |
| 125 | + | |
| 126 | +| Séquence | Nom | Effet | | |
| 127 | +| --- | --- | --- | | |
| 128 | +| `CSI n A B C D` | CUU CUD CUF CUB | Déplacer de n cellules haut, bas, droite, gauche | | |
| 129 | +| `CSI n E F` | CNL CPL | n lignes plus bas / plus haut, colonne 1 | | |
| 130 | +| `CSI n G` | CHA | Aller à la colonne n | | |
| 131 | +| `CSI l ; c H`, `CSI l ; c f` | CUP HVP | Aller à la ligne l, colonne c | | |
| 132 | +| `CSI n d` | VPA | Aller à la ligne n | | |
| 133 | +| `CSI n J` | ED | Effacer l'écran : 0 jusqu'à la fin, 1 jusqu'au début, 2 ou 3 tout | | |
| 134 | +| `CSI n K` | EL | Effacer la ligne : 0 jusqu'à la fin, 1 jusqu'au début, 2 tout | | |
| 135 | +| `CSI n L` | IL | Insérer n lignes vides au curseur | | |
| 136 | +| `CSI n M` | DL | Supprimer n lignes au curseur | | |
| 137 | +| `CSI n @` | ICH | Insérer n cellules vides | | |
| 138 | +| `CSI n P` | DCH | Supprimer n cellules | | |
| 139 | +| `CSI n X` | ECH | Effacer n cellules sur place | | |
| 140 | +| `CSI n S` | SU | Faire défiler la région de n lignes vers le haut | | |
| 141 | +| `CSI n T` | SD | Faire défiler la région de n lignes vers le bas | | |
| 142 | +| `CSI h ; b r` | DECSTBM | Définir la région de défilement aux lignes h…b | | |
| 143 | +| `CSI s`, `CSI u` | SCP RCP | Sauvegarder / restaurer le curseur | | |
| 144 | +| `CSI … m` | SGR | Couleurs et attributs, ci-dessous | | |
| 145 | + | |
| 146 | +`IL` et `DL` ne font rien lorsque le curseur est hors de la région de défilement. | |
| 147 | + | |
| 148 | +### Modes privés | |
| 149 | + | |
| 150 | +Activés par `CSI ? n h`, désactivés par `CSI ? n l`. | |
| 151 | + | |
| 152 | +| n | Nom | Effet | | |
| 153 | +| --- | --- | --- | | |
| 154 | +| 1 | DECCKM | Touches curseur application : les flèches envoient `ESC O x` | | |
| 155 | +| 7 | DECAWM | Retour à la ligne automatique à la marge droite | | |
| 156 | +| 25 | DECTCEM | Afficher le curseur | | |
| 157 | +| 47, 1047 | | Écran alternatif | | |
| 158 | +| 1048 | | Sauvegarder / restaurer le curseur | | |
| 159 | +| 1049 | | Sauvegarder le curseur, puis l'écran alternatif | | |
| 160 | + | |
| 161 | +Tout autre mode est analysé et ignoré. | |
| 162 | + | |
| 163 | +### SGR | |
| 164 | + | |
| 165 | +| Code | Effet | | |
| 166 | +| --- | --- | | |
| 167 | +| 0 | Réinitialisation | | |
| 168 | +| 1, 22 | Gras activé / désactivé | | |
| 169 | +| 2, 22 | Atténué activé / désactivé | | |
| 170 | +| 3, 23 | Italique activé / désactivé | | |
| 171 | +| 4, 24 | Souligné activé / désactivé | | |
| 172 | +| 5, 6, 25 | Clignotement activé / désactivé | | |
| 173 | +| 7, 27 | Vidéo inverse activée / désactivée | | |
| 174 | +| 9, 29 | Barré activé / désactivé | | |
| 175 | +| 30–37, 40–47 | Les huit couleurs normales, premier plan / fond | | |
| 176 | +| 90–97, 100–107 | Les huit couleurs vives, premier plan / fond | | |
| 177 | +| 38;5;n, 48;5;n | Couleur n de la palette de 256 | | |
| 178 | +| 38;2;r;g;b, 48;2;r;g;b | Couleur 24 bits | | |
| 179 | +| 39, 49 | Retour à la couleur du thème | | |
| 180 | + | |
| 181 | +Les seize couleurs nommées sont celles de tcell, c'est-à-dire la palette configurée dans le terminal de l'utilisateur, et non des valeurs hexadécimales figées. Une couleur étendue à court de paramètres laisse le style inchangé. Tout autre code est ignoré. | |
| 182 | + | |
| 183 | +### OSC | |
| 184 | + | |
| 185 | +| Séquence | Effet | | |
| 186 | +| --- | --- | | |
| 187 | +| `OSC 0 ; texte BEL`, `OSC 2 ; texte BEL` | Définir le titre de la fenêtre | | |
| 188 | +| `OSC … ST` | Le terminateur `ESC \` est accepté à la place de BEL | | |
| 189 | + | |
| 190 | +Le titre est plafonné à 4096 octets. Les autres commandes OSC sont analysées et ignorées. | |
| 191 | + | |
| 192 | +### Consommées et ignorées | |
| 193 | + | |
| 194 | +Analysées correctement, donc jamais affichées comme des caractères parasites, mais sans effet : | |
| 195 | + | |
| 196 | +| Séquence | Nom | | |
| 197 | +| --- | --- | | |
| 198 | +| `ESC P …`, `ESC X …`, `ESC ^ …`, `ESC _ …` | DCS, SOS, PM, APC — lues jusqu'à leur terminateur de chaîne | | |
| 199 | +| `ESC (`, `ESC )`, `ESC *`, `ESC +`, `ESC %`, `ESC #`, `ESC <espace>` | Sélecteurs de jeu de caractères et de taille de ligne — l'émulateur travaille en UTF-8 de toute façon | | |
| 200 | +| `CSI ? n h`, `CSI ? n l` pour tout autre n | Modes privés non listés ci-dessus | | |
| 201 | +| Tout autre octet final CSI, code SGR ou commande OSC | | | |
| 202 | + | |
| 203 | +### Non implémenté | |
| 204 | + | |
| 205 | +Le rapport souris, le collage entre crochets, les bascules shift-in / shift-out, les lignes double largeur, sixel et les autres protocoles graphiques, ainsi que les rapports d'état et d'attributs DEC. Un programme qui en demande un n'obtient aucune réponse : celui qui en attend une attendra indéfiniment. | |
| 206 | + | |
| 207 | +## Couleurs | |
| 208 | + | |
| 209 | +| Clé de thème | Ce qu'elle colore | | |
| 210 | +| --- | --- | | |
| 211 | +| `terminal.text` | Toute cellule dont le programme n'a pas choisi la couleur | | |
| 212 | +| `terminal.cursor` | La cellule sous le curseur, quand la fenêtre a le focus | | |
| 213 | + | |
| 214 | +Voir [Format des fichiers de thème](themes.md). | |
| 215 | + | |
| 216 | +## Erreurs | |
| 217 | + | |
| 218 | +| Message | Cause | | |
| 219 | +| --- | --- | | |
| 220 | +| Terminal windows are not supported on this platform yet | La compilation n'a pas de support des pseudo-terminaux : toute plateforme autre que Linux, macOS et Windows | | |
| 221 | +| `openpt: …`, `grantpt: …`, `ptsname: …` | Le système d'exploitation a refusé d'ouvrir un pseudo-terminal | | |
| 222 | +| `fork/exec …: no such file or directory` | `$SHELL` désigne un programme inexistant | | |
| 223 | + | |
| 224 | +## Voir aussi | |
| 225 | + | |
| 226 | +- [Lancer des commandes shell sans quitter l'éditeur](../how-to/use-a-terminal.md) | |
| 227 | +- [Fenêtres terminal](../explanation/terminal-windows.md) | |
| 228 | +- [Clavier](keyboard.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,228 @@ | |||
| 1 | +# Référence : fenêtres terminal | ||
| 2 | + | ||
| 3 | +> Description neutre des fenêtres terminal ouvertes par Turbo Golo, des touches auxquelles elles répondent et des séquences d'échappement que l'émulateur implémente. | ||
| 4 | + | ||
| 5 | +## Ouverture | ||
| 6 | + | ||
| 7 | +| Chemin | Condition | | ||
| 8 | +| --- | --- | | ||
| 9 | +| `F8` | Toujours | | ||
| 10 | +| **Window ▸ New terminal** | Toujours | | ||
| 11 | + | ||
| 12 | +Aucun des deux n'exige qu'un fichier soit ouvert. Les outils du menu **Golo** dont la sortie est `terminal` — le REPL, le débogueur, l'exécution d'un script — ouvrent eux aussi une fenêtre terminal ; voir [Outils Golo](golo-tools.md). | ||
| 13 | + | ||
| 14 | +## Le shell | ||
| 15 | + | ||
| 16 | +| Propriété | Valeur | | ||
| 17 | +| --- | --- | | ||
| 18 | +| Programme | `$SHELL`, ou `/bin/sh` si la variable est absente ou vide ; sous Windows `%COMSPEC%`, ou `cmd.exe` | | ||
| 19 | +| Répertoire de travail | Le dossier du fichier de la fenêtre au premier plan ; le répertoire de travail de l'éditeur si aucun fichier n'est ouvert | | ||
| 20 | +| `TERM` | `xterm-256color`, toujours — remplaçant toute valeur héritée | | ||
| 21 | +| Environnement | Celui de l'éditeur, avec `TERM` remplacé | | ||
| 22 | +| Terminal de contrôle | Oui : sous Linux et macOS le shell tourne dans sa propre session avec le pseudo-terminal comme terminal de contrôle ; sous Windows il est attaché à une pseudo-console. Dans les deux cas le contrôle de tâches et `Ctrl-C` fonctionnent | | ||
| 23 | +| Taille initiale | Celle de la fenêtre, mise à jour à chaque redimensionnement | | ||
| 24 | + | ||
| 25 | +## Plateformes supportées | ||
| 26 | + | ||
| 27 | +| Plateforme | Comportement | | ||
| 28 | +| --- | --- | | ||
| 29 | +| Linux | Supportée (`/dev/ptmx`) | | ||
| 30 | +| macOS | Supportée (`/dev/ptmx`) | | ||
| 31 | +| Windows | Supportée (pseudo-console, ConPTY) : Windows 10 version 1809 ou plus récent. Compilé et vérifié ; **pas encore exécuté par les auteurs** sur une machine Windows | | ||
| 32 | +| Autres | `F8` ouvre un message indiquant que les fenêtres terminal ne sont pas encore supportées ; rien d'autre ne change | | ||
| 33 | + | ||
| 34 | +## Touches | ||
| 35 | + | ||
| 36 | +### Après la fin du programme | ||
| 37 | + | ||
| 38 | +Une fenêtre dont la commande est terminée garde sa sortie mais cesse de se comporter comme un terminal : seules `Maj-Page↑` et `Maj-Page↓` sont encore prises, et toute autre touche atteint l'éditeur — c'est ce qui permet à `Ctrl-W` de la fermer. | ||
| 39 | + | ||
| 40 | +### Envoyées au shell | ||
| 41 | + | ||
| 42 | +Toute touche non listée ci-dessous sous « conservées par l'éditeur », encodée comme un terminal l'attend. | ||
| 43 | + | ||
| 44 | +| Touche | Octets envoyés | | ||
| 45 | +| --- | --- | | ||
| 46 | +| caractère imprimable | son encodage UTF-8 | | ||
| 47 | +| `Alt-<touche>` | `ESC` suivi des octets de cette touche | | ||
| 48 | +| `Ctrl-A` … `Ctrl-Z` | `0x01` … `0x1a` | | ||
| 49 | +| `Enter` | `\r` | | ||
| 50 | +| `Tab` | `\t` | | ||
| 51 | +| `Shift-Tab` | `ESC [ Z` | | ||
| 52 | +| `Backspace` | `0x7f` | | ||
| 53 | +| `Escape` | `0x1b` | | ||
| 54 | +| `↑` `↓` `→` `←` | `ESC [ A B C D`, ou `ESC O A B C D` en mode curseur application | | ||
| 55 | +| `Home` `End` | `ESC [ H`, `ESC [ F`, ou les formes `ESC O` en mode curseur application | | ||
| 56 | +| `Insert` `Delete` | `ESC [ 2~`, `ESC [ 3~` | | ||
| 57 | +| `PgUp` `PgDn` | `ESC [ 5~`, `ESC [ 6~` | | ||
| 58 | +| `F1` … `F4` | `ESC O P Q R S` | | ||
| 59 | +| `F5` … `F12` | `ESC [ 15~ 17~ 18~ 19~ 20~ 21~ 23~ 24~` | | ||
| 60 | + | ||
| 61 | +Une touche sans signification pour un terminal n'envoie rien. | ||
| 62 | + | ||
| 63 | +### Conservées par l'éditeur | ||
| 64 | + | ||
| 65 | +| Touche | Action | | ||
| 66 | +| --- | --- | | ||
| 67 | +| `F1` … `F12` | Leur action habituelle dans l'éditeur | | ||
| 68 | +| `Alt-X` | Quitter | | ||
| 69 | +| `Alt-0` … `Alt-9` | Lister les fenêtres / passer la fenêtre 1…9 au premier plan | | ||
| 70 | + | ||
| 71 | +Les touches de fonction n'atteignent donc jamais un programme lancé dans une fenêtre terminal. | ||
| 72 | + | ||
| 73 | +### Traitées par la fenêtre terminal elle-même | ||
| 74 | + | ||
| 75 | +| Touche | Action | | ||
| 76 | +| --- | --- | | ||
| 77 | +| `Shift-PgUp` | Reculer d'un écran dans l'historique | | ||
| 78 | +| `Shift-PgDn` | Avancer d'un écran | | ||
| 79 | + | ||
| 80 | +Toute touche envoyée au shell ramène également la vue à l'écran vivant. | ||
| 81 | + | ||
| 82 | +## Souris | ||
| 83 | + | ||
| 84 | +| Action | Effet | | ||
| 85 | +| --- | --- | | ||
| 86 | +| Molette haut / bas | Défiler de trois lignes dans l'historique | | ||
| 87 | +| Clic | Passe la fenêtre au premier plan ; n'est pas transmis au programme | | ||
| 88 | + | ||
| 89 | +Le rapport souris n'est pas implémenté : un programme n'est jamais informé des clics. | ||
| 90 | + | ||
| 91 | +## Historique | ||
| 92 | + | ||
| 93 | +| Propriété | Valeur | | ||
| 94 | +| --- | --- | | ||
| 95 | +| Lignes conservées | 2000 | | ||
| 96 | +| Ce qui est conservé | Uniquement les lignes sorties par le haut de l'écran principal | | ||
| 97 | +| Écran alternatif | Non conservé — un programme plein écran ne laisse aucun historique | | ||
| 98 | + | ||
| 99 | +## Émulation | ||
| 100 | + | ||
| 101 | +`TERM` vaut `xterm-256color`. Voici ce qui en est implémenté. | ||
| 102 | + | ||
| 103 | +### Caractères de contrôle | ||
| 104 | + | ||
| 105 | +| Octet | Effet | | ||
| 106 | +| --- | --- | | ||
| 107 | +| `0x07` BEL | Noté ; l'éditeur ne l'émet pas | | ||
| 108 | +| `0x08` BS | Curseur d'une colonne à gauche | | ||
| 109 | +| `0x09` HT | Jusqu'à la tabulation suivante, toutes les 8 colonnes | | ||
| 110 | +| `0x0a` `0x0b` `0x0c` | Saut de ligne | | ||
| 111 | +| `0x0d` CR | Colonne 1 | | ||
| 112 | + | ||
| 113 | +### Séquences d'échappement | ||
| 114 | + | ||
| 115 | +| Séquence | Nom | Effet | | ||
| 116 | +| --- | --- | --- | | ||
| 117 | +| `ESC D` | IND | Saut de ligne | | ||
| 118 | +| `ESC E` | NEL | Retour chariot et saut de ligne | | ||
| 119 | +| `ESC M` | RI | Saut de ligne inverse, en conservant la colonne | | ||
| 120 | +| `ESC 7` | DECSC | Sauvegarder le curseur et le style | | ||
| 121 | +| `ESC 8` | DECRC | Les restaurer | | ||
| 122 | +| `ESC c` | RIS | Réinitialisation complète | | ||
| 123 | + | ||
| 124 | +### Séquences CSI | ||
| 125 | + | ||
| 126 | +| Séquence | Nom | Effet | | ||
| 127 | +| --- | --- | --- | | ||
| 128 | +| `CSI n A B C D` | CUU CUD CUF CUB | Déplacer de n cellules haut, bas, droite, gauche | | ||
| 129 | +| `CSI n E F` | CNL CPL | n lignes plus bas / plus haut, colonne 1 | | ||
| 130 | +| `CSI n G` | CHA | Aller à la colonne n | | ||
| 131 | +| `CSI l ; c H`, `CSI l ; c f` | CUP HVP | Aller à la ligne l, colonne c | | ||
| 132 | +| `CSI n d` | VPA | Aller à la ligne n | | ||
| 133 | +| `CSI n J` | ED | Effacer l'écran : 0 jusqu'à la fin, 1 jusqu'au début, 2 ou 3 tout | | ||
| 134 | +| `CSI n K` | EL | Effacer la ligne : 0 jusqu'à la fin, 1 jusqu'au début, 2 tout | | ||
| 135 | +| `CSI n L` | IL | Insérer n lignes vides au curseur | | ||
| 136 | +| `CSI n M` | DL | Supprimer n lignes au curseur | | ||
| 137 | +| `CSI n @` | ICH | Insérer n cellules vides | | ||
| 138 | +| `CSI n P` | DCH | Supprimer n cellules | | ||
| 139 | +| `CSI n X` | ECH | Effacer n cellules sur place | | ||
| 140 | +| `CSI n S` | SU | Faire défiler la région de n lignes vers le haut | | ||
| 141 | +| `CSI n T` | SD | Faire défiler la région de n lignes vers le bas | | ||
| 142 | +| `CSI h ; b r` | DECSTBM | Définir la région de défilement aux lignes h…b | | ||
| 143 | +| `CSI s`, `CSI u` | SCP RCP | Sauvegarder / restaurer le curseur | | ||
| 144 | +| `CSI … m` | SGR | Couleurs et attributs, ci-dessous | | ||
| 145 | + | ||
| 146 | +`IL` et `DL` ne font rien lorsque le curseur est hors de la région de défilement. | ||
| 147 | + | ||
| 148 | +### Modes privés | ||
| 149 | + | ||
| 150 | +Activés par `CSI ? n h`, désactivés par `CSI ? n l`. | ||
| 151 | + | ||
| 152 | +| n | Nom | Effet | | ||
| 153 | +| --- | --- | --- | | ||
| 154 | +| 1 | DECCKM | Touches curseur application : les flèches envoient `ESC O x` | | ||
| 155 | +| 7 | DECAWM | Retour à la ligne automatique à la marge droite | | ||
| 156 | +| 25 | DECTCEM | Afficher le curseur | | ||
| 157 | +| 47, 1047 | | Écran alternatif | | ||
| 158 | +| 1048 | | Sauvegarder / restaurer le curseur | | ||
| 159 | +| 1049 | | Sauvegarder le curseur, puis l'écran alternatif | | ||
| 160 | + | ||
| 161 | +Tout autre mode est analysé et ignoré. | ||
| 162 | + | ||
| 163 | +### SGR | ||
| 164 | + | ||
| 165 | +| Code | Effet | | ||
| 166 | +| --- | --- | | ||
| 167 | +| 0 | Réinitialisation | | ||
| 168 | +| 1, 22 | Gras activé / désactivé | | ||
| 169 | +| 2, 22 | Atténué activé / désactivé | | ||
| 170 | +| 3, 23 | Italique activé / désactivé | | ||
| 171 | +| 4, 24 | Souligné activé / désactivé | | ||
| 172 | +| 5, 6, 25 | Clignotement activé / désactivé | | ||
| 173 | +| 7, 27 | Vidéo inverse activée / désactivée | | ||
| 174 | +| 9, 29 | Barré activé / désactivé | | ||
| 175 | +| 30–37, 40–47 | Les huit couleurs normales, premier plan / fond | | ||
| 176 | +| 90–97, 100–107 | Les huit couleurs vives, premier plan / fond | | ||
| 177 | +| 38;5;n, 48;5;n | Couleur n de la palette de 256 | | ||
| 178 | +| 38;2;r;g;b, 48;2;r;g;b | Couleur 24 bits | | ||
| 179 | +| 39, 49 | Retour à la couleur du thème | | ||
| 180 | + | ||
| 181 | +Les seize couleurs nommées sont celles de tcell, c'est-à-dire la palette configurée dans le terminal de l'utilisateur, et non des valeurs hexadécimales figées. Une couleur étendue à court de paramètres laisse le style inchangé. Tout autre code est ignoré. | ||
| 182 | + | ||
| 183 | +### OSC | ||
| 184 | + | ||
| 185 | +| Séquence | Effet | | ||
| 186 | +| --- | --- | | ||
| 187 | +| `OSC 0 ; texte BEL`, `OSC 2 ; texte BEL` | Définir le titre de la fenêtre | | ||
| 188 | +| `OSC … ST` | Le terminateur `ESC \` est accepté à la place de BEL | | ||
| 189 | + | ||
| 190 | +Le titre est plafonné à 4096 octets. Les autres commandes OSC sont analysées et ignorées. | ||
| 191 | + | ||
| 192 | +### Consommées et ignorées | ||
| 193 | + | ||
| 194 | +Analysées correctement, donc jamais affichées comme des caractères parasites, mais sans effet : | ||
| 195 | + | ||
| 196 | +| Séquence | Nom | | ||
| 197 | +| --- | --- | | ||
| 198 | +| `ESC P …`, `ESC X …`, `ESC ^ …`, `ESC _ …` | DCS, SOS, PM, APC — lues jusqu'à leur terminateur de chaîne | | ||
| 199 | +| `ESC (`, `ESC )`, `ESC *`, `ESC +`, `ESC %`, `ESC #`, `ESC <espace>` | Sélecteurs de jeu de caractères et de taille de ligne — l'émulateur travaille en UTF-8 de toute façon | | ||
| 200 | +| `CSI ? n h`, `CSI ? n l` pour tout autre n | Modes privés non listés ci-dessus | | ||
| 201 | +| Tout autre octet final CSI, code SGR ou commande OSC | | | ||
| 202 | + | ||
| 203 | +### Non implémenté | ||
| 204 | + | ||
| 205 | +Le rapport souris, le collage entre crochets, les bascules shift-in / shift-out, les lignes double largeur, sixel et les autres protocoles graphiques, ainsi que les rapports d'état et d'attributs DEC. Un programme qui en demande un n'obtient aucune réponse : celui qui en attend une attendra indéfiniment. | ||
| 206 | + | ||
| 207 | +## Couleurs | ||
| 208 | + | ||
| 209 | +| Clé de thème | Ce qu'elle colore | | ||
| 210 | +| --- | --- | | ||
| 211 | +| `terminal.text` | Toute cellule dont le programme n'a pas choisi la couleur | | ||
| 212 | +| `terminal.cursor` | La cellule sous le curseur, quand la fenêtre a le focus | | ||
| 213 | + | ||
| 214 | +Voir [Format des fichiers de thème](themes.md). | ||
| 215 | + | ||
| 216 | +## Erreurs | ||
| 217 | + | ||
| 218 | +| Message | Cause | | ||
| 219 | +| --- | --- | | ||
| 220 | +| Terminal windows are not supported on this platform yet | La compilation n'a pas de support des pseudo-terminaux : toute plateforme autre que Linux, macOS et Windows | | ||
| 221 | +| `openpt: …`, `grantpt: …`, `ptsname: …` | Le système d'exploitation a refusé d'ouvrir un pseudo-terminal | | ||
| 222 | +| `fork/exec …: no such file or directory` | `$SHELL` désigne un programme inexistant | | ||
| 223 | + | ||
| 224 | +## Voir aussi | ||
| 225 | + | ||
| 226 | +- [Lancer des commandes shell sans quitter l'éditeur](../how-to/use-a-terminal.md) | ||
| 227 | +- [Fenêtres terminal](../explanation/terminal-windows.md) | ||
| 228 | +- [Clavier](keyboard.md) | ||
added
docs/fr/reference/themes.md +239 -0 | new file mode 100644 | ||
| @@ -0,0 +1,239 @@ | ||
| 1 | +# Référence : format des fichiers de thème | |
| 2 | + | |
| 3 | +> Description neutre et exhaustive d'un fichier de thème Turbo Golo. | |
| 4 | + | |
| 5 | +Un thème est un fichier TOML. Les thèmes sont lus d'abord dans le répertoire utilisateur, puis parmi ceux embarqués dans le binaire ; un fichier utilisateur l'emporte sur un thème embarqué du même nom. | |
| 6 | + | |
| 7 | +## Emplacements | |
| 8 | + | |
| 9 | +| Emplacement | Remarques | | |
| 10 | +| --- | --- | | |
| 11 | +| `$TURBO_GOLO_THEME_DIR` | Utilisé quand la variable est définie et non vide. | | |
| 12 | +| `$TURBO_GOLO_DIR/themes` | Quand `TURBO_GOLO_DIR` est définie, à la place du répertoire de configuration de la plateforme. | | |
| 13 | +| `~/.config/turbo-golo/themes` | Linux (`os.UserConfigDir`). | | |
| 14 | +| `~/Library/Application Support/turbo-golo/themes` | macOS. | | |
| 15 | +| embarqués | `turbo-classic`, `turbo-dark`, `borland-light`, `cappuccino`, `catppuccin-frappe`, `catppuccin-latte`, `cobalt`, `darcula`, `intellij-light`, `monochrome-dark`, `monochrome-light`. | | |
| 16 | + | |
| 17 | +Le **nom** d'un thème pour `-theme` et pour `Options ▸ Theme…` est son nom de fichier sans `.toml`. Il ne peut contenir ni `/`, ni `\`, ni `..`. | |
| 18 | + | |
| 19 | +## Les thèmes livrés | |
| 20 | + | |
| 21 | +| Nom | Fond | Pour | | |
| 22 | +| --- | --- | --- | | |
| 23 | +| `turbo-classic` | Marine Borland | Le défaut : la palette de Turbo C | | |
| 24 | +| `turbo-dark` | Gris sombre neutre | Les terminaux modernes en couleurs vraies | | |
| 25 | +| `borland-light` | Blanc papier | Les pièces claires et la vidéoprojection | | |
| 26 | +| `cappuccino` | Brun expresso | La mise en page Turbo, en chaud : du lait dans le texte, du caramel là où Turbo Dark met du bleu | | |
| 27 | +| `catppuccin-frappe` | Ardoise chaude | La palette Catppuccin Frappé, inchangée : des accents pastel sur un fond doucement sombre | | |
| 28 | +| `catppuccin-latte` | Papier chaud | La palette Catppuccin Latte, inchangée : le même mappage avec la saturation qu'exige un fond clair | | |
| 29 | +| `cobalt` | Marine profond | La palette Cobalt, accents laissés aussi francs qu'on les connaît | | |
| 30 | +| `darcula` | Anthracite | D'après le Darcula de JetBrains : mots-clés orange, chaînes vertes, et la ponctuation orange qui le rend reconnaissable | | |
| 31 | +| `intellij-light` | Blanc | D'après l'IntelliJ Light de JetBrains : mots-clés bleus gras, chaînes vertes grasses | | |
| 32 | +| `monochrome-dark` | Noir et gris | Aucune teinte — le code se distingue par la luminosité, le gras, l'italique et le souligné | | |
| 33 | +| `monochrome-light` | Papier et gris | Le même, dans l'autre sens : sur papier, c'est le gris le plus foncé qui parle le plus fort | | |
| 34 | + | |
| 35 | +Chacun **énonce sa palette entière** au lieu d'en hériter l'essentiel. Un thème que vous écrivez, lui, peut hériter ; voir [en écrire un](../how-to/write-a-theme.md). | |
| 36 | + | |
| 37 | +## Un nom auquel un thème répondait autrefois | |
| 38 | + | |
| 39 | +`monochrome` se charge toujours. C'est le nom sous lequel ce thème était livré avant que `monochrome-light` ne le rejoigne et que la paire ne soit renommée : un fichier de réglages ou un `-theme` disant `monochrome` obtient `monochrome-dark`. | |
| 40 | + | |
| 41 | +| Nom retiré | Charge | | |
| 42 | +| --- | --- | | |
| 43 | +| `monochrome` | `monochrome-dark` | | |
| 44 | + | |
| 45 | +Un nom retiré n'est **pas** listé par `-list-themes` ni par **Options ▸ Theme…** : chaque thème n'apparaît donc qu'une fois, sous le nom qu'il porte aujourd'hui. Un thème à vous nommé `monochrome.toml` l'emporte quand même sur lui, exactement comme pour n'importe quel autre nom. | |
| 46 | + | |
| 47 | +## Champs de premier niveau | |
| 48 | + | |
| 49 | +| Champ | Type | Défaut | Description | | |
| 50 | +| --- | --- | --- | --- | | |
| 51 | +| `name` | chaîne | le nom du fichier | Nom affiché, dans le sélecteur de thème et la boîte À propos. | | |
| 52 | +| `description` | chaîne | `""` | Une ligne, affichée par `-list-themes`. | | |
| 53 | +| `inherits` | chaîne | aucun | Nom d'un thème dont partir. Ses styles résolus servent de base ; ce fichier redéfinit ce qu'il nomme. Les chaînes sont plafonnées à 16 sauts. | | |
| 54 | +| `colors` | table | `{}` | Les styles. Les clés sont celles listées plus bas. | | |
| 55 | + | |
| 56 | +## Champs d'une entrée | |
| 57 | + | |
| 58 | +Chaque valeur sous `[colors]` est une table en ligne : | |
| 59 | + | |
| 60 | +| Champ | Type | Défaut | Description | | |
| 61 | +| --- | --- | --- | --- | | |
| 62 | +| `fg` | chaîne | hérité | Couleur de premier plan. | | |
| 63 | +| `bg` | chaîne | hérité | Couleur de fond. | | |
| 64 | +| `bold` | booléen | `false` | Activer le gras. | | |
| 65 | +| `underline` | booléen | `false` | Activer le souligné. | | |
| 66 | +| `italic` | booléen | `false` | Activer l'italique. | | |
| 67 | +| `reverse` | booléen | `false` | Échanger premier plan et fond. | | |
| 68 | +| `dim` | booléen | `false` | Activer l'atténuation. | | |
| 69 | +| `blink` | booléen | `false` | Activer le clignotement. | | |
| 70 | + | |
| 71 | +Les attributs ne sont jamais qu'**activés** ; il n'existe pas de moyen de désactiver un attribut hérité autrement qu'en n'en héritant pas. | |
| 72 | + | |
| 73 | +## Valeurs de couleur | |
| 74 | + | |
| 75 | +| Forme | Exemple | Remarques | | |
| 76 | +| --- | --- | --- | | |
| 77 | +| Nom ANSI | `navy`, `aqua`, `silver`, `fuchsia` | Les seize noms, plus la liste W3C complète. | | |
| 78 | +| Littéral hexadécimal | `#5fafd7` | 24 bits ; tcell l'approxime sur les terminaux sans couleurs vraies. | | |
| 79 | +| `default` | `default` | Ce que le terminal utilise lui-même. | | |
| 80 | +| `-` | `-` | Identique à `default`. | | |
| 81 | +| `""` | `""` | Identique à `default`. | | |
| 82 | + | |
| 83 | +Les seize noms ANSI : `black` `maroon` `green` `olive` `navy` `purple` `teal` `silver` `gray` `red` `lime` `yellow` `blue` `fuchsia` `aqua` `white`. | |
| 84 | + | |
| 85 | +Une couleur non reconnue est une **erreur de chargement**, pas un repli silencieux. | |
| 86 | + | |
| 87 | +## Clés de style | |
| 88 | + | |
| 89 | +Les clés non définies retombent le long des points, et finalement sur `default`. | |
| 90 | + | |
| 91 | +### Base | |
| 92 | + | |
| 93 | +| Clé | Ce qu'elle colore | | |
| 94 | +| --- | --- | | |
| 95 | +| `default` | Le dernier recours de toute recherche | | |
| 96 | +| `desktop` | Le fond texturé derrière les fenêtres | | |
| 97 | +| `shadow` | Les cellules qu'une fenêtre assombrit derrière elle | | |
| 98 | + | |
| 99 | +### Barre de menus | |
| 100 | + | |
| 101 | +| Clé | Ce qu'elle colore | | |
| 102 | +| --- | --- | | |
| 103 | +| `menu.bar` | La rangée de titres | | |
| 104 | +| `menu.item` | Une entrée déroulante | | |
| 105 | +| `menu.selected` | L'entrée surlignée | | |
| 106 | +| `menu.shortcut` | La lettre d'accès d'un intitulé | | |
| 107 | +| `menu.disabled` | Une entrée non choisissable | | |
| 108 | + | |
| 109 | +### Fenêtres | |
| 110 | + | |
| 111 | +| Clé | Ce qu'elle colore | | |
| 112 | +| --- | --- | | |
| 113 | +| `window.frame.active` | Le cadre de la fenêtre active | | |
| 114 | +| `window.frame.inactive` | Tous les autres cadres | | |
| 115 | +| `window.title.active` | Le titre de la fenêtre active | | |
| 116 | +| `window.title.inactive` | Tous les autres titres | | |
| 117 | +| `window.body` | L'intérieur, avant que son contenu ne se dessine | | |
| 118 | + | |
| 119 | +### Barres | |
| 120 | + | |
| 121 | +| Clé | Ce qu'elle colore | | |
| 122 | +| --- | --- | | |
| 123 | +| `statusbar` | La barre elle-même | | |
| 124 | +| `statusbar.key` | La partie `Fn` d'un indice | | |
| 125 | +| `statusbar.hint` | Le texte aligné à droite | | |
| 126 | +| `scrollbar` | La glissière d'une barre de défilement | | |
| 127 | +| `scrollbar.thumb` | Son curseur et ses flèches | | |
| 128 | + | |
| 129 | +### Dialogues et contrôles | |
| 130 | + | |
| 131 | +| Clé | Ce qu'elle colore | | |
| 132 | +| --- | --- | | |
| 133 | +| `dialog.frame` | Le cadre d'un dialogue | | |
| 134 | +| `dialog.body` | Son intérieur | | |
| 135 | +| `dialog.title` | Son titre | | |
| 136 | +| `dialog.label` | Une ligne de texte statique | | |
| 137 | +| `button` | Un bouton | | |
| 138 | +| `button.focused` | Le bouton qui a le focus | | |
| 139 | +| `button.shortcut` | La lettre d'accès d'un bouton | | |
| 140 | +| `input` | Un champ de saisie | | |
| 141 | +| `input.focused` | Le champ qui a le focus | | |
| 142 | +| `input.selection` | Le texte sélectionné dans un champ | | |
| 143 | +| `list` | Une liste | | |
| 144 | +| `list.selected` | Sa ligne surlignée, quand elle a le focus | | |
| 145 | +| `list.unfocused` | Sa ligne surlignée, sinon | | |
| 146 | +| `checkbox` | Une case à cocher | | |
| 147 | +| `checkbox.focused` | La case qui a le focus | | |
| 148 | + | |
| 149 | +### Éditeur | |
| 150 | + | |
| 151 | +| Clé | Ce qu'elle colore | | |
| 152 | +| --- | --- | | |
| 153 | +| `editor.text` | Le texte qu'aucune autre règle ne revendique | | |
| 154 | +| `editor.selection` | Le texte sélectionné | | |
| 155 | +| `editor.linenumber` | La gouttière des numéros de ligne | | |
| 156 | +| `editor.currentline` | La ligne où se trouve le curseur | | |
| 157 | +| `editor.cursor` | Le curseur. Son **fond** est aussi envoyé au terminal comme couleur de curseur, et son premier plan peint le caractère en dessous. | | |
| 158 | + | |
| 159 | +### Terminal | |
| 160 | + | |
| 161 | +| Clé | Ce qu'elle colore | | |
| 162 | +| --- | --- | | |
| 163 | +| `terminal.text` | Toute cellule d'une fenêtre terminal dont le programme qui y tourne n'a pas choisi la couleur | | |
| 164 | +| `terminal.cursor` | La cellule sous le curseur d'un terminal, quand cette fenêtre a le focus | | |
| 165 | + | |
| 166 | +Un programme qui nomme ses propres couleurs les conserve : ces deux clés ne remplissent que ce qu'il a laissé indéfini. Voir [Fenêtres terminal](terminal.md). | |
| 167 | + | |
| 168 | +### Arbre du projet | |
| 169 | + | |
| 170 | +| Clé | Ce qu'elle colore | | |
| 171 | +| --- | --- | | |
| 172 | +| `tree.text` | Le nom d'un fichier dans l'arbre, et le fond de l'arbre | | |
| 173 | +| `tree.directory` | Le nom d'un dossier | | |
| 174 | +| `tree.selected` | La ligne surlignée, quand l'arbre a le focus | | |
| 175 | +| `tree.unfocused` | La ligne surlignée, quand il ne l'a pas | | |
| 176 | + | |
| 177 | +Elles sont distinctes des clés `list.*` à dessein : la liste d'un dialogue est colorée pour ressortir sur un dialogue, et la réutiliser surlignerait une ligne d'arbre dans la couleur même qu'a déjà le corps d'une fenêtre. Voir [Arbre du projet](project-tree.md). | |
| 178 | + | |
| 179 | +### Syntaxe | |
| 180 | + | |
| 181 | +Les exemples sont ceux du Golo ; les autres langages produisent les mêmes classes à leur façon. | |
| 182 | + | |
| 183 | +| Clé | Ce qu'elle colore | | |
| 184 | +| --- | --- | | |
| 185 | +| `syntax.identifier` | Un nom ordinaire | | |
| 186 | +| `syntax.keyword` | `function`, `let`, `module`, `foreach`, … | | |
| 187 | +| `syntax.type` | Un nom qui commence par une majuscule — `Point`, `Shape`, `Some` — et le chemin après `module` ou `import` | | |
| 188 | +| `syntax.builtin` | `println`, `len`, `list`, `map`, … | | |
| 189 | +| `syntax.constant` | `true`, `false`, `null` | | |
| 190 | +| `syntax.function` | Un nom en minuscules avant `(`, ou le nom après `function` | | |
| 191 | +| `syntax.string` | Un littéral chaîne, `"…"` ou `"""…"""` | | |
| 192 | +| `syntax.char` | Un littéral caractère, `'c'` | | |
| 193 | +| `syntax.number` | Un littéral entier ou flottant, suffixe `L` ou `F` compris | | |
| 194 | +| `syntax.comment` | `#` jusqu'à la fin de la ligne, et `----` … `----` | | |
| 195 | +| `syntax.operator` | `+`, `->`, `?:`, `..`, … | | |
| 196 | +| `syntax.punctuation` | Parenthèses, crochets, accolades, virgules, points, points-virgules et `$` | | |
| 197 | +| `syntax.heading` | Un titre Markdown, toute la ligne | | |
| 198 | +| `syntax.tag` | Un nom d'élément HTML et ses chevrons | | |
| 199 | +| `syntax.attribute` | Le nom d'un attribut HTML | | |
| 200 | +| `syntax.emphasis` | Le gras et l'italique Markdown | | |
| 201 | +| `syntax.link` | Un lien ou une image Markdown | | |
| 202 | + | |
| 203 | +Quel langage produit quelle classe est indiqué dans [Langages colorés](languages.md). | |
| 204 | + | |
| 205 | +### Complétion et diagnostics | |
| 206 | + | |
| 207 | +| Clé | Ce qu'elle colore | | |
| 208 | +| --- | --- | | |
| 209 | +| `completion.frame` | Le cadre de la liste | | |
| 210 | +| `completion.item` | Une suggestion | | |
| 211 | +| `completion.selected` | La suggestion surlignée | | |
| 212 | +| `completion.detail` | L'étiquette de nature à côté d'une suggestion | | |
| 213 | +| `diagnostic.error` | Une erreur du serveur de langage | | |
| 214 | +| `diagnostic.warning` | Un avertissement | | |
| 215 | +| `diagnostic.info` | Une note | | |
| 216 | + | |
| 217 | +## Exemple | |
| 218 | + | |
| 219 | +```toml | |
| 220 | +name = "Le mien" | |
| 221 | +description = "Turbo Classic, avec des commentaires lisibles." | |
| 222 | +inherits = "turbo-classic" | |
| 223 | + | |
| 224 | +[colors] | |
| 225 | +"syntax.comment" = { fg = "#8a8a8a", italic = true } | |
| 226 | +"syntax.string" = { fg = "#87d7af" } | |
| 227 | +"editor.currentline" = { bg = "#00005f" } | |
| 228 | +``` | |
| 229 | + | |
| 230 | +## Erreurs | |
| 231 | + | |
| 232 | +| Message | Cause | | |
| 233 | +| --- | --- | | |
| 234 | +| `theme: not found: "x"` | Aucun `x.toml` dans le répertoire utilisateur ni parmi les thèmes embarqués. | | |
| 235 | +| `theme: not found: "…" is not a plain theme name` | Le nom contient `/`, `\` ou `..`. | | |
| 236 | +| `invalid TOML: …` | Le fichier n'est pas du TOML valide. | | |
| 237 | +| `colors."k": fg: unknown colour "…"` | Le nom de couleur n'est pas reconnu. | | |
| 238 | +| `inherits: chain deeper than 16, probably a loop` | Deux thèmes héritent l'un de l'autre, directement ou par l'intermédiaire d'autres. | | |
| 239 | +| `inherits "x": theme: not found` | Le parent nommé n'existe pas. | | |
| new file mode 100644 | |||
| @@ -0,0 +1,239 @@ | |||
| 1 | +# Référence : format des fichiers de thème | ||
| 2 | + | ||
| 3 | +> Description neutre et exhaustive d'un fichier de thème Turbo Golo. | ||
| 4 | + | ||
| 5 | +Un thème est un fichier TOML. Les thèmes sont lus d'abord dans le répertoire utilisateur, puis parmi ceux embarqués dans le binaire ; un fichier utilisateur l'emporte sur un thème embarqué du même nom. | ||
| 6 | + | ||
| 7 | +## Emplacements | ||
| 8 | + | ||
| 9 | +| Emplacement | Remarques | | ||
| 10 | +| --- | --- | | ||
| 11 | +| `$TURBO_GOLO_THEME_DIR` | Utilisé quand la variable est définie et non vide. | | ||
| 12 | +| `$TURBO_GOLO_DIR/themes` | Quand `TURBO_GOLO_DIR` est définie, à la place du répertoire de configuration de la plateforme. | | ||
| 13 | +| `~/.config/turbo-golo/themes` | Linux (`os.UserConfigDir`). | | ||
| 14 | +| `~/Library/Application Support/turbo-golo/themes` | macOS. | | ||
| 15 | +| embarqués | `turbo-classic`, `turbo-dark`, `borland-light`, `cappuccino`, `catppuccin-frappe`, `catppuccin-latte`, `cobalt`, `darcula`, `intellij-light`, `monochrome-dark`, `monochrome-light`. | | ||
| 16 | + | ||
| 17 | +Le **nom** d'un thème pour `-theme` et pour `Options ▸ Theme…` est son nom de fichier sans `.toml`. Il ne peut contenir ni `/`, ni `\`, ni `..`. | ||
| 18 | + | ||
| 19 | +## Les thèmes livrés | ||
| 20 | + | ||
| 21 | +| Nom | Fond | Pour | | ||
| 22 | +| --- | --- | --- | | ||
| 23 | +| `turbo-classic` | Marine Borland | Le défaut : la palette de Turbo C | | ||
| 24 | +| `turbo-dark` | Gris sombre neutre | Les terminaux modernes en couleurs vraies | | ||
| 25 | +| `borland-light` | Blanc papier | Les pièces claires et la vidéoprojection | | ||
| 26 | +| `cappuccino` | Brun expresso | La mise en page Turbo, en chaud : du lait dans le texte, du caramel là où Turbo Dark met du bleu | | ||
| 27 | +| `catppuccin-frappe` | Ardoise chaude | La palette Catppuccin Frappé, inchangée : des accents pastel sur un fond doucement sombre | | ||
| 28 | +| `catppuccin-latte` | Papier chaud | La palette Catppuccin Latte, inchangée : le même mappage avec la saturation qu'exige un fond clair | | ||
| 29 | +| `cobalt` | Marine profond | La palette Cobalt, accents laissés aussi francs qu'on les connaît | | ||
| 30 | +| `darcula` | Anthracite | D'après le Darcula de JetBrains : mots-clés orange, chaînes vertes, et la ponctuation orange qui le rend reconnaissable | | ||
| 31 | +| `intellij-light` | Blanc | D'après l'IntelliJ Light de JetBrains : mots-clés bleus gras, chaînes vertes grasses | | ||
| 32 | +| `monochrome-dark` | Noir et gris | Aucune teinte — le code se distingue par la luminosité, le gras, l'italique et le souligné | | ||
| 33 | +| `monochrome-light` | Papier et gris | Le même, dans l'autre sens : sur papier, c'est le gris le plus foncé qui parle le plus fort | | ||
| 34 | + | ||
| 35 | +Chacun **énonce sa palette entière** au lieu d'en hériter l'essentiel. Un thème que vous écrivez, lui, peut hériter ; voir [en écrire un](../how-to/write-a-theme.md). | ||
| 36 | + | ||
| 37 | +## Un nom auquel un thème répondait autrefois | ||
| 38 | + | ||
| 39 | +`monochrome` se charge toujours. C'est le nom sous lequel ce thème était livré avant que `monochrome-light` ne le rejoigne et que la paire ne soit renommée : un fichier de réglages ou un `-theme` disant `monochrome` obtient `monochrome-dark`. | ||
| 40 | + | ||
| 41 | +| Nom retiré | Charge | | ||
| 42 | +| --- | --- | | ||
| 43 | +| `monochrome` | `monochrome-dark` | | ||
| 44 | + | ||
| 45 | +Un nom retiré n'est **pas** listé par `-list-themes` ni par **Options ▸ Theme…** : chaque thème n'apparaît donc qu'une fois, sous le nom qu'il porte aujourd'hui. Un thème à vous nommé `monochrome.toml` l'emporte quand même sur lui, exactement comme pour n'importe quel autre nom. | ||
| 46 | + | ||
| 47 | +## Champs de premier niveau | ||
| 48 | + | ||
| 49 | +| Champ | Type | Défaut | Description | | ||
| 50 | +| --- | --- | --- | --- | | ||
| 51 | +| `name` | chaîne | le nom du fichier | Nom affiché, dans le sélecteur de thème et la boîte À propos. | | ||
| 52 | +| `description` | chaîne | `""` | Une ligne, affichée par `-list-themes`. | | ||
| 53 | +| `inherits` | chaîne | aucun | Nom d'un thème dont partir. Ses styles résolus servent de base ; ce fichier redéfinit ce qu'il nomme. Les chaînes sont plafonnées à 16 sauts. | | ||
| 54 | +| `colors` | table | `{}` | Les styles. Les clés sont celles listées plus bas. | | ||
| 55 | + | ||
| 56 | +## Champs d'une entrée | ||
| 57 | + | ||
| 58 | +Chaque valeur sous `[colors]` est une table en ligne : | ||
| 59 | + | ||
| 60 | +| Champ | Type | Défaut | Description | | ||
| 61 | +| --- | --- | --- | --- | | ||
| 62 | +| `fg` | chaîne | hérité | Couleur de premier plan. | | ||
| 63 | +| `bg` | chaîne | hérité | Couleur de fond. | | ||
| 64 | +| `bold` | booléen | `false` | Activer le gras. | | ||
| 65 | +| `underline` | booléen | `false` | Activer le souligné. | | ||
| 66 | +| `italic` | booléen | `false` | Activer l'italique. | | ||
| 67 | +| `reverse` | booléen | `false` | Échanger premier plan et fond. | | ||
| 68 | +| `dim` | booléen | `false` | Activer l'atténuation. | | ||
| 69 | +| `blink` | booléen | `false` | Activer le clignotement. | | ||
| 70 | + | ||
| 71 | +Les attributs ne sont jamais qu'**activés** ; il n'existe pas de moyen de désactiver un attribut hérité autrement qu'en n'en héritant pas. | ||
| 72 | + | ||
| 73 | +## Valeurs de couleur | ||
| 74 | + | ||
| 75 | +| Forme | Exemple | Remarques | | ||
| 76 | +| --- | --- | --- | | ||
| 77 | +| Nom ANSI | `navy`, `aqua`, `silver`, `fuchsia` | Les seize noms, plus la liste W3C complète. | | ||
| 78 | +| Littéral hexadécimal | `#5fafd7` | 24 bits ; tcell l'approxime sur les terminaux sans couleurs vraies. | | ||
| 79 | +| `default` | `default` | Ce que le terminal utilise lui-même. | | ||
| 80 | +| `-` | `-` | Identique à `default`. | | ||
| 81 | +| `""` | `""` | Identique à `default`. | | ||
| 82 | + | ||
| 83 | +Les seize noms ANSI : `black` `maroon` `green` `olive` `navy` `purple` `teal` `silver` `gray` `red` `lime` `yellow` `blue` `fuchsia` `aqua` `white`. | ||
| 84 | + | ||
| 85 | +Une couleur non reconnue est une **erreur de chargement**, pas un repli silencieux. | ||
| 86 | + | ||
| 87 | +## Clés de style | ||
| 88 | + | ||
| 89 | +Les clés non définies retombent le long des points, et finalement sur `default`. | ||
| 90 | + | ||
| 91 | +### Base | ||
| 92 | + | ||
| 93 | +| Clé | Ce qu'elle colore | | ||
| 94 | +| --- | --- | | ||
| 95 | +| `default` | Le dernier recours de toute recherche | | ||
| 96 | +| `desktop` | Le fond texturé derrière les fenêtres | | ||
| 97 | +| `shadow` | Les cellules qu'une fenêtre assombrit derrière elle | | ||
| 98 | + | ||
| 99 | +### Barre de menus | ||
| 100 | + | ||
| 101 | +| Clé | Ce qu'elle colore | | ||
| 102 | +| --- | --- | | ||
| 103 | +| `menu.bar` | La rangée de titres | | ||
| 104 | +| `menu.item` | Une entrée déroulante | | ||
| 105 | +| `menu.selected` | L'entrée surlignée | | ||
| 106 | +| `menu.shortcut` | La lettre d'accès d'un intitulé | | ||
| 107 | +| `menu.disabled` | Une entrée non choisissable | | ||
| 108 | + | ||
| 109 | +### Fenêtres | ||
| 110 | + | ||
| 111 | +| Clé | Ce qu'elle colore | | ||
| 112 | +| --- | --- | | ||
| 113 | +| `window.frame.active` | Le cadre de la fenêtre active | | ||
| 114 | +| `window.frame.inactive` | Tous les autres cadres | | ||
| 115 | +| `window.title.active` | Le titre de la fenêtre active | | ||
| 116 | +| `window.title.inactive` | Tous les autres titres | | ||
| 117 | +| `window.body` | L'intérieur, avant que son contenu ne se dessine | | ||
| 118 | + | ||
| 119 | +### Barres | ||
| 120 | + | ||
| 121 | +| Clé | Ce qu'elle colore | | ||
| 122 | +| --- | --- | | ||
| 123 | +| `statusbar` | La barre elle-même | | ||
| 124 | +| `statusbar.key` | La partie `Fn` d'un indice | | ||
| 125 | +| `statusbar.hint` | Le texte aligné à droite | | ||
| 126 | +| `scrollbar` | La glissière d'une barre de défilement | | ||
| 127 | +| `scrollbar.thumb` | Son curseur et ses flèches | | ||
| 128 | + | ||
| 129 | +### Dialogues et contrôles | ||
| 130 | + | ||
| 131 | +| Clé | Ce qu'elle colore | | ||
| 132 | +| --- | --- | | ||
| 133 | +| `dialog.frame` | Le cadre d'un dialogue | | ||
| 134 | +| `dialog.body` | Son intérieur | | ||
| 135 | +| `dialog.title` | Son titre | | ||
| 136 | +| `dialog.label` | Une ligne de texte statique | | ||
| 137 | +| `button` | Un bouton | | ||
| 138 | +| `button.focused` | Le bouton qui a le focus | | ||
| 139 | +| `button.shortcut` | La lettre d'accès d'un bouton | | ||
| 140 | +| `input` | Un champ de saisie | | ||
| 141 | +| `input.focused` | Le champ qui a le focus | | ||
| 142 | +| `input.selection` | Le texte sélectionné dans un champ | | ||
| 143 | +| `list` | Une liste | | ||
| 144 | +| `list.selected` | Sa ligne surlignée, quand elle a le focus | | ||
| 145 | +| `list.unfocused` | Sa ligne surlignée, sinon | | ||
| 146 | +| `checkbox` | Une case à cocher | | ||
| 147 | +| `checkbox.focused` | La case qui a le focus | | ||
| 148 | + | ||
| 149 | +### Éditeur | ||
| 150 | + | ||
| 151 | +| Clé | Ce qu'elle colore | | ||
| 152 | +| --- | --- | | ||
| 153 | +| `editor.text` | Le texte qu'aucune autre règle ne revendique | | ||
| 154 | +| `editor.selection` | Le texte sélectionné | | ||
| 155 | +| `editor.linenumber` | La gouttière des numéros de ligne | | ||
| 156 | +| `editor.currentline` | La ligne où se trouve le curseur | | ||
| 157 | +| `editor.cursor` | Le curseur. Son **fond** est aussi envoyé au terminal comme couleur de curseur, et son premier plan peint le caractère en dessous. | | ||
| 158 | + | ||
| 159 | +### Terminal | ||
| 160 | + | ||
| 161 | +| Clé | Ce qu'elle colore | | ||
| 162 | +| --- | --- | | ||
| 163 | +| `terminal.text` | Toute cellule d'une fenêtre terminal dont le programme qui y tourne n'a pas choisi la couleur | | ||
| 164 | +| `terminal.cursor` | La cellule sous le curseur d'un terminal, quand cette fenêtre a le focus | | ||
| 165 | + | ||
| 166 | +Un programme qui nomme ses propres couleurs les conserve : ces deux clés ne remplissent que ce qu'il a laissé indéfini. Voir [Fenêtres terminal](terminal.md). | ||
| 167 | + | ||
| 168 | +### Arbre du projet | ||
| 169 | + | ||
| 170 | +| Clé | Ce qu'elle colore | | ||
| 171 | +| --- | --- | | ||
| 172 | +| `tree.text` | Le nom d'un fichier dans l'arbre, et le fond de l'arbre | | ||
| 173 | +| `tree.directory` | Le nom d'un dossier | | ||
| 174 | +| `tree.selected` | La ligne surlignée, quand l'arbre a le focus | | ||
| 175 | +| `tree.unfocused` | La ligne surlignée, quand il ne l'a pas | | ||
| 176 | + | ||
| 177 | +Elles sont distinctes des clés `list.*` à dessein : la liste d'un dialogue est colorée pour ressortir sur un dialogue, et la réutiliser surlignerait une ligne d'arbre dans la couleur même qu'a déjà le corps d'une fenêtre. Voir [Arbre du projet](project-tree.md). | ||
| 178 | + | ||
| 179 | +### Syntaxe | ||
| 180 | + | ||
| 181 | +Les exemples sont ceux du Golo ; les autres langages produisent les mêmes classes à leur façon. | ||
| 182 | + | ||
| 183 | +| Clé | Ce qu'elle colore | | ||
| 184 | +| --- | --- | | ||
| 185 | +| `syntax.identifier` | Un nom ordinaire | | ||
| 186 | +| `syntax.keyword` | `function`, `let`, `module`, `foreach`, … | | ||
| 187 | +| `syntax.type` | Un nom qui commence par une majuscule — `Point`, `Shape`, `Some` — et le chemin après `module` ou `import` | | ||
| 188 | +| `syntax.builtin` | `println`, `len`, `list`, `map`, … | | ||
| 189 | +| `syntax.constant` | `true`, `false`, `null` | | ||
| 190 | +| `syntax.function` | Un nom en minuscules avant `(`, ou le nom après `function` | | ||
| 191 | +| `syntax.string` | Un littéral chaîne, `"…"` ou `"""…"""` | | ||
| 192 | +| `syntax.char` | Un littéral caractère, `'c'` | | ||
| 193 | +| `syntax.number` | Un littéral entier ou flottant, suffixe `L` ou `F` compris | | ||
| 194 | +| `syntax.comment` | `#` jusqu'à la fin de la ligne, et `----` … `----` | | ||
| 195 | +| `syntax.operator` | `+`, `->`, `?:`, `..`, … | | ||
| 196 | +| `syntax.punctuation` | Parenthèses, crochets, accolades, virgules, points, points-virgules et `$` | | ||
| 197 | +| `syntax.heading` | Un titre Markdown, toute la ligne | | ||
| 198 | +| `syntax.tag` | Un nom d'élément HTML et ses chevrons | | ||
| 199 | +| `syntax.attribute` | Le nom d'un attribut HTML | | ||
| 200 | +| `syntax.emphasis` | Le gras et l'italique Markdown | | ||
| 201 | +| `syntax.link` | Un lien ou une image Markdown | | ||
| 202 | + | ||
| 203 | +Quel langage produit quelle classe est indiqué dans [Langages colorés](languages.md). | ||
| 204 | + | ||
| 205 | +### Complétion et diagnostics | ||
| 206 | + | ||
| 207 | +| Clé | Ce qu'elle colore | | ||
| 208 | +| --- | --- | | ||
| 209 | +| `completion.frame` | Le cadre de la liste | | ||
| 210 | +| `completion.item` | Une suggestion | | ||
| 211 | +| `completion.selected` | La suggestion surlignée | | ||
| 212 | +| `completion.detail` | L'étiquette de nature à côté d'une suggestion | | ||
| 213 | +| `diagnostic.error` | Une erreur du serveur de langage | | ||
| 214 | +| `diagnostic.warning` | Un avertissement | | ||
| 215 | +| `diagnostic.info` | Une note | | ||
| 216 | + | ||
| 217 | +## Exemple | ||
| 218 | + | ||
| 219 | +```toml | ||
| 220 | +name = "Le mien" | ||
| 221 | +description = "Turbo Classic, avec des commentaires lisibles." | ||
| 222 | +inherits = "turbo-classic" | ||
| 223 | + | ||
| 224 | +[colors] | ||
| 225 | +"syntax.comment" = { fg = "#8a8a8a", italic = true } | ||
| 226 | +"syntax.string" = { fg = "#87d7af" } | ||
| 227 | +"editor.currentline" = { bg = "#00005f" } | ||
| 228 | +``` | ||
| 229 | + | ||
| 230 | +## Erreurs | ||
| 231 | + | ||
| 232 | +| Message | Cause | | ||
| 233 | +| --- | --- | | ||
| 234 | +| `theme: not found: "x"` | Aucun `x.toml` dans le répertoire utilisateur ni parmi les thèmes embarqués. | | ||
| 235 | +| `theme: not found: "…" is not a plain theme name` | Le nom contient `/`, `\` ou `..`. | | ||
| 236 | +| `invalid TOML: …` | Le fichier n'est pas du TOML valide. | | ||
| 237 | +| `colors."k": fg: unknown colour "…"` | Le nom de couleur n'est pas reconnu. | | ||
| 238 | +| `inherits: chain deeper than 16, probably a loop` | Deux thèmes héritent l'un de l'autre, directement ou par l'intermédiaire d'autres. | | ||
| 239 | +| `inherits "x": theme: not found` | Le parent nommé n'existe pas. | | ||
added
docs/fr/reference/versioning.md +150 -0 | new file mode 100644 | ||
| @@ -0,0 +1,150 @@ | ||
| 1 | +# Référence : le numéro de version | |
| 2 | + | |
| 3 | +> Description neutre de l'origine de la version que Turbo Golo annonce, et de ce que produit chaque façon de le construire. | |
| 4 | + | |
| 5 | +## D'où vient le numéro | |
| 6 | + | |
| 7 | +Trois sources, consultées dans cet ordre. La première qui répond l'emporte. | |
| 8 | + | |
| 9 | +| Ordre | Source | Renseignée par | | |
| 10 | +| --- | --- | --- | | |
| 11 | +| 1 | Estampilles de l'éditeur de liens | `make build`, `make install`, `scripts/install.sh` | | |
| 12 | +| 2 | Informations de build de Go | L'outil Go, automatiquement | | |
| 13 | +| 3 | `unknown` | Rien — la valeur annoncée quand aucune source n'a pu nommer le build | | |
| 14 | + | |
| 15 | +Il n'y a **aucune constante de version dans les sources**. Un numéro écrit dans un fichier `.go` doit être modifié dans le cadre d'une release, et devient faux dès que quelqu'un l'oublie. | |
| 16 | + | |
| 17 | +## Estampilles de l'éditeur de liens | |
| 18 | + | |
| 19 | +Trois variables du paquet `version` de turbo-core — la bibliothèque que tous les éditeurs de la famille partagent, et donc le même chemin pour chacun d'eux —, renseignées par `-ldflags -X`. | |
| 20 | + | |
| 21 | +| Variable | Remplie depuis | Exemple | | |
| 22 | +| --- | --- | --- | | |
| 23 | +| `stamp` | `git describe --tags --dirty` | `v0.1.0-14-g88a4c38` | | |
| 24 | +| `commit` | `git rev-parse --short HEAD` | `88a4c38` | | |
| 25 | +| `built` | `date -u +%Y-%m-%dT%H:%M:%SZ` | `2026-08-31T18:04:05Z` | | |
| 26 | + | |
| 27 | +```sh | |
| 28 | +go build -ldflags "\ | |
| 29 | + -X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' \ | |
| 30 | + -X 'rickub.com/turbo-editors/turbo-core/version.commit=88a4c38' \ | |
| 31 | + -X 'rickub.com/turbo-editors/turbo-core/version.built=2026-08-31T18:04:05Z'" . | |
| 32 | +``` | |
| 33 | + | |
| 34 | +Un `v` initial est retiré à l'affichage : le tag est `v0.2.0`, la boîte About affiche `0.2.0`. | |
| 35 | + | |
| 36 | +## Informations de build de Go | |
| 37 | + | |
| 38 | +Lues via `runtime/debug.ReadBuildInfo()` quand rien n'a été estampillé. | |
| 39 | + | |
| 40 | +| Champ lu | Sert à | | |
| 41 | +| --- | --- | | |
| 42 | +| `Main.Version` | Le numéro, sauf s'il est vide, `(devel)`, ou une pseudo-version | | |
| 43 | +| `vcs.revision` | Le commit, abrégé à sept caractères | | |
| 44 | +| `vcs.modified` | L'ajout ou non du suffixe `-dirty` | | |
| 45 | + | |
| 46 | +`vcs.time` n'est **pas** utilisé. Il enregistre la date du commit, pas celle de l'édition de liens ; l'annoncer comme date de build serait faux sur tout binaire construit après son propre commit. | |
| 47 | + | |
| 48 | +Une **pseudo-version** — `v0.1.1-0.20260831165958-88a4c3859bf3` — est la façon dont l'outil Go nomme un commit qu'aucun tag ne nomme. Elle est rapportée comme `devel`, et non affichée telle quelle : son `0.1.1` est un correctif qui n'existe pas. | |
| 49 | + | |
| 50 | +## Ce qu'annonce chaque build | |
| 51 | + | |
| 52 | +| Construit par | Numéro | Commit | Date | | |
| 53 | +| --- | --- | --- | --- | | |
| 54 | +| `make build`, `make install`, `scripts/install.sh` | `0.1.0-14-g88a4c38` | oui | oui | | |
| 55 | +| Les mêmes, sur un commit tagué | `0.2.0` | oui | oui | | |
| 56 | +| Les mêmes, avec des modifications non validées | `0.1.0-14-g88a4c38-dirty` | oui | oui | | |
| 57 | +| `go install rickub.com/turbo-editors/turbo-golo@v0.2.0` | `0.2.0` | non | non | | |
| 58 | +| `go build .` dans un dépôt cloné | `devel` | oui | non | | |
| 59 | +| `go build .` dans un dépôt cloné avec des modifications | `devel-dirty` | oui | non | | |
| 60 | +| `go run .` | `unknown` | non | non | | |
| 61 | +| Un dossier sans git, et sans estampille | `unknown` | non | non | | |
| 62 | + | |
| 63 | +Seules les lignes estampillées peuvent annoncer un tag : le système de build de Go ne lit pas les tags git. | |
| 64 | + | |
| 65 | +## Vérifié au moment du build | |
| 66 | + | |
| 67 | +Une estampille d'édition de liens est une chaîne de caractères, et une mauvaise n'est pas une erreur. Un `-X` qui nomme un symbole inexistant s'édite sans se plaindre et n'estampille rien ; le binaire retombe alors sur les informations de build de Go et annonce une version que le build n'a jamais voulue — souvent `devel`, sur un binaire attaché à une release. Rien d'autre que l'exécution du binaire ne le détecte : chaque build qui en produit un l'exécute donc. | |
| 68 | + | |
| 69 | +C'est `scripts/check-version.sh` qui s'en charge. | |
| 70 | + | |
| 71 | +| Appelé par | Sur | Un échec fait échouer | | |
| 72 | +| --- | --- | --- | | |
| 73 | +| `make build` | `bin/turbo-golo`, avec `$(VERSION)` et `$(COMMIT)` | le build | | |
| 74 | +| `scripts/install.sh` | le binaire en attente, **avant** son installation | l'installation, en laissant intact celui qui est déjà là | | |
| 75 | +| `03-build-releases.sh` | le seul artefact en attente que cette machine sait exécuter, avec le tag | la construction de la release | | |
| 76 | + | |
| 77 | +```sh | |
| 78 | +scripts/check-version.sh bin/turbo-golo v0.2.0 88a4c38 # un build estampillé | |
| 79 | +scripts/check-version.sh bin/turbo-golo # rien à attendre | |
| 80 | +``` | |
| 81 | + | |
| 82 | +| Arguments | Réussit si | | |
| 83 | +| --- | --- | | |
| 84 | +| binaire, version, commit | le numéro annoncé est **égal** à la version privée de son `v` initial, et le commit apparaît dans la sortie | | |
| 85 | +| binaire, version | le numéro lui est égal | | |
| 86 | +| binaire | le numéro est autre chose qu'`unknown` | | |
| 87 | + | |
| 88 | +La comparaison de version est une égalité, pas une recherche. `0.2.0` est une sous-chaîne de `10.2.0`, et d'une empreinte de commit qui le contiendrait par hasard ; une estampille presque juste est précisément ce que cette vérification existe pour attraper. | |
| 89 | + | |
| 90 | +| Code de sortie | Signification | | |
| 91 | +| --- | --- | | |
| 92 | +| `0` | Le binaire annonce ce que le build voulait. La ligne qu'il a affichée est réémise. | | |
| 93 | +| `1` | Il ne s'exécute pas, n'est pas là, ou annonce autre chose. | | |
| 94 | +| `2` | Aucun binaire n'a été nommé. | | |
| 95 | + | |
| 96 | +## Où il s'affiche | |
| 97 | + | |
| 98 | +### `-version` | |
| 99 | + | |
| 100 | +Une ligne, portant chaque élément connu. | |
| 101 | + | |
| 102 | +``` | |
| 103 | +Turbo Golo 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z) | |
| 104 | +Turbo Golo 0.2.0 (88a4c38) | |
| 105 | +Turbo Golo 0.2.0 | |
| 106 | +``` | |
| 107 | + | |
| 108 | +### Help ▸ About | |
| 109 | + | |
| 110 | +Une ligne par fait connu. Un fait que le build n'a pas enregistré n'a **pas de ligne**, plutôt qu'une ligne vide. | |
| 111 | + | |
| 112 | +``` | |
| 113 | +Turbo Golo 0.2.0 | |
| 114 | + | |
| 115 | +A Turbo C-style editor for Golo, | |
| 116 | +written in Go. | |
| 117 | + | |
| 118 | +Commit: 88a4c38 | |
| 119 | +Built: 2026-08-31 18:04 UTC | |
| 120 | +Theme: Turbo Classic | |
| 121 | +``` | |
| 122 | + | |
| 123 | +`Built` est rendu en UTC sous la forme `AAAA-MM-JJ HH:MM UTC`. Une estampille qui n'est pas un RFC 3339 valide est affichée exactement telle qu'elle a été donnée, plutôt que supprimée. | |
| 124 | + | |
| 125 | +### `make version` | |
| 126 | + | |
| 127 | +Affiche ce que ce dépôt estampillerait, sans construire. | |
| 128 | + | |
| 129 | +``` | |
| 130 | +$ make version | |
| 131 | +v0.1.0-14-g88a4c38 (88a4c38) | |
| 132 | +``` | |
| 133 | + | |
| 134 | +### `make ldflags` | |
| 135 | + | |
| 136 | +Affiche les options d'édition de liens qu'utilise un build estampillé, pour qu'un script puisse les réutiliser au lieu de répéter les chemins `-X`. | |
| 137 | + | |
| 138 | +``` | |
| 139 | +$ make ldflags | |
| 140 | +-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' -X '….commit=7f8b36a' -X '….built=2026-08-31T19:02:03Z' | |
| 141 | +``` | |
| 142 | + | |
| 143 | +`03-build-releases.sh` les lit pour ses compilations croisées, en surchargeant la version par le tag qu'il publie — `make ldflags VERSION=v0.2.0` — pour que les binaires disent ce que dit la release plutôt que ce que dit `git describe`. Un binaire compilé sans elles annonce `devel`, quoi que dise la release à laquelle il est attaché. | |
| 144 | + | |
| 145 | +## Voir aussi | |
| 146 | + | |
| 147 | +- Faire une release pour que le numéro soit juste : [Comment faire une release](../how-to/make-a-release.md) | |
| 148 | +- Ce à quoi `-version` ne sert **pas** : il est écrit pour un humain. Un script qui a besoin du numéro doit comparer avec `grep -F`, ou interroger git, plutôt que d'en extraire un champ. | |
| 149 | +- Pourquoi il n'y a pas de constante de version : [Décisions de conception](../explanation/design-decisions.md#la-version-est-une-propriété-du-build-pas-des-sources) | |
| 150 | +- L'option `-version` parmi les autres : [Ligne de commande](cli.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,150 @@ | |||
| 1 | +# Référence : le numéro de version | ||
| 2 | + | ||
| 3 | +> Description neutre de l'origine de la version que Turbo Golo annonce, et de ce que produit chaque façon de le construire. | ||
| 4 | + | ||
| 5 | +## D'où vient le numéro | ||
| 6 | + | ||
| 7 | +Trois sources, consultées dans cet ordre. La première qui répond l'emporte. | ||
| 8 | + | ||
| 9 | +| Ordre | Source | Renseignée par | | ||
| 10 | +| --- | --- | --- | | ||
| 11 | +| 1 | Estampilles de l'éditeur de liens | `make build`, `make install`, `scripts/install.sh` | | ||
| 12 | +| 2 | Informations de build de Go | L'outil Go, automatiquement | | ||
| 13 | +| 3 | `unknown` | Rien — la valeur annoncée quand aucune source n'a pu nommer le build | | ||
| 14 | + | ||
| 15 | +Il n'y a **aucune constante de version dans les sources**. Un numéro écrit dans un fichier `.go` doit être modifié dans le cadre d'une release, et devient faux dès que quelqu'un l'oublie. | ||
| 16 | + | ||
| 17 | +## Estampilles de l'éditeur de liens | ||
| 18 | + | ||
| 19 | +Trois variables du paquet `version` de turbo-core — la bibliothèque que tous les éditeurs de la famille partagent, et donc le même chemin pour chacun d'eux —, renseignées par `-ldflags -X`. | ||
| 20 | + | ||
| 21 | +| Variable | Remplie depuis | Exemple | | ||
| 22 | +| --- | --- | --- | | ||
| 23 | +| `stamp` | `git describe --tags --dirty` | `v0.1.0-14-g88a4c38` | | ||
| 24 | +| `commit` | `git rev-parse --short HEAD` | `88a4c38` | | ||
| 25 | +| `built` | `date -u +%Y-%m-%dT%H:%M:%SZ` | `2026-08-31T18:04:05Z` | | ||
| 26 | + | ||
| 27 | +```sh | ||
| 28 | +go build -ldflags "\ | ||
| 29 | + -X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' \ | ||
| 30 | + -X 'rickub.com/turbo-editors/turbo-core/version.commit=88a4c38' \ | ||
| 31 | + -X 'rickub.com/turbo-editors/turbo-core/version.built=2026-08-31T18:04:05Z'" . | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +Un `v` initial est retiré à l'affichage : le tag est `v0.2.0`, la boîte About affiche `0.2.0`. | ||
| 35 | + | ||
| 36 | +## Informations de build de Go | ||
| 37 | + | ||
| 38 | +Lues via `runtime/debug.ReadBuildInfo()` quand rien n'a été estampillé. | ||
| 39 | + | ||
| 40 | +| Champ lu | Sert à | | ||
| 41 | +| --- | --- | | ||
| 42 | +| `Main.Version` | Le numéro, sauf s'il est vide, `(devel)`, ou une pseudo-version | | ||
| 43 | +| `vcs.revision` | Le commit, abrégé à sept caractères | | ||
| 44 | +| `vcs.modified` | L'ajout ou non du suffixe `-dirty` | | ||
| 45 | + | ||
| 46 | +`vcs.time` n'est **pas** utilisé. Il enregistre la date du commit, pas celle de l'édition de liens ; l'annoncer comme date de build serait faux sur tout binaire construit après son propre commit. | ||
| 47 | + | ||
| 48 | +Une **pseudo-version** — `v0.1.1-0.20260831165958-88a4c3859bf3` — est la façon dont l'outil Go nomme un commit qu'aucun tag ne nomme. Elle est rapportée comme `devel`, et non affichée telle quelle : son `0.1.1` est un correctif qui n'existe pas. | ||
| 49 | + | ||
| 50 | +## Ce qu'annonce chaque build | ||
| 51 | + | ||
| 52 | +| Construit par | Numéro | Commit | Date | | ||
| 53 | +| --- | --- | --- | --- | | ||
| 54 | +| `make build`, `make install`, `scripts/install.sh` | `0.1.0-14-g88a4c38` | oui | oui | | ||
| 55 | +| Les mêmes, sur un commit tagué | `0.2.0` | oui | oui | | ||
| 56 | +| Les mêmes, avec des modifications non validées | `0.1.0-14-g88a4c38-dirty` | oui | oui | | ||
| 57 | +| `go install rickub.com/turbo-editors/turbo-golo@v0.2.0` | `0.2.0` | non | non | | ||
| 58 | +| `go build .` dans un dépôt cloné | `devel` | oui | non | | ||
| 59 | +| `go build .` dans un dépôt cloné avec des modifications | `devel-dirty` | oui | non | | ||
| 60 | +| `go run .` | `unknown` | non | non | | ||
| 61 | +| Un dossier sans git, et sans estampille | `unknown` | non | non | | ||
| 62 | + | ||
| 63 | +Seules les lignes estampillées peuvent annoncer un tag : le système de build de Go ne lit pas les tags git. | ||
| 64 | + | ||
| 65 | +## Vérifié au moment du build | ||
| 66 | + | ||
| 67 | +Une estampille d'édition de liens est une chaîne de caractères, et une mauvaise n'est pas une erreur. Un `-X` qui nomme un symbole inexistant s'édite sans se plaindre et n'estampille rien ; le binaire retombe alors sur les informations de build de Go et annonce une version que le build n'a jamais voulue — souvent `devel`, sur un binaire attaché à une release. Rien d'autre que l'exécution du binaire ne le détecte : chaque build qui en produit un l'exécute donc. | ||
| 68 | + | ||
| 69 | +C'est `scripts/check-version.sh` qui s'en charge. | ||
| 70 | + | ||
| 71 | +| Appelé par | Sur | Un échec fait échouer | | ||
| 72 | +| --- | --- | --- | | ||
| 73 | +| `make build` | `bin/turbo-golo`, avec `$(VERSION)` et `$(COMMIT)` | le build | | ||
| 74 | +| `scripts/install.sh` | le binaire en attente, **avant** son installation | l'installation, en laissant intact celui qui est déjà là | | ||
| 75 | +| `03-build-releases.sh` | le seul artefact en attente que cette machine sait exécuter, avec le tag | la construction de la release | | ||
| 76 | + | ||
| 77 | +```sh | ||
| 78 | +scripts/check-version.sh bin/turbo-golo v0.2.0 88a4c38 # un build estampillé | ||
| 79 | +scripts/check-version.sh bin/turbo-golo # rien à attendre | ||
| 80 | +``` | ||
| 81 | + | ||
| 82 | +| Arguments | Réussit si | | ||
| 83 | +| --- | --- | | ||
| 84 | +| binaire, version, commit | le numéro annoncé est **égal** à la version privée de son `v` initial, et le commit apparaît dans la sortie | | ||
| 85 | +| binaire, version | le numéro lui est égal | | ||
| 86 | +| binaire | le numéro est autre chose qu'`unknown` | | ||
| 87 | + | ||
| 88 | +La comparaison de version est une égalité, pas une recherche. `0.2.0` est une sous-chaîne de `10.2.0`, et d'une empreinte de commit qui le contiendrait par hasard ; une estampille presque juste est précisément ce que cette vérification existe pour attraper. | ||
| 89 | + | ||
| 90 | +| Code de sortie | Signification | | ||
| 91 | +| --- | --- | | ||
| 92 | +| `0` | Le binaire annonce ce que le build voulait. La ligne qu'il a affichée est réémise. | | ||
| 93 | +| `1` | Il ne s'exécute pas, n'est pas là, ou annonce autre chose. | | ||
| 94 | +| `2` | Aucun binaire n'a été nommé. | | ||
| 95 | + | ||
| 96 | +## Où il s'affiche | ||
| 97 | + | ||
| 98 | +### `-version` | ||
| 99 | + | ||
| 100 | +Une ligne, portant chaque élément connu. | ||
| 101 | + | ||
| 102 | +``` | ||
| 103 | +Turbo Golo 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z) | ||
| 104 | +Turbo Golo 0.2.0 (88a4c38) | ||
| 105 | +Turbo Golo 0.2.0 | ||
| 106 | +``` | ||
| 107 | + | ||
| 108 | +### Help ▸ About | ||
| 109 | + | ||
| 110 | +Une ligne par fait connu. Un fait que le build n'a pas enregistré n'a **pas de ligne**, plutôt qu'une ligne vide. | ||
| 111 | + | ||
| 112 | +``` | ||
| 113 | +Turbo Golo 0.2.0 | ||
| 114 | + | ||
| 115 | +A Turbo C-style editor for Golo, | ||
| 116 | +written in Go. | ||
| 117 | + | ||
| 118 | +Commit: 88a4c38 | ||
| 119 | +Built: 2026-08-31 18:04 UTC | ||
| 120 | +Theme: Turbo Classic | ||
| 121 | +``` | ||
| 122 | + | ||
| 123 | +`Built` est rendu en UTC sous la forme `AAAA-MM-JJ HH:MM UTC`. Une estampille qui n'est pas un RFC 3339 valide est affichée exactement telle qu'elle a été donnée, plutôt que supprimée. | ||
| 124 | + | ||
| 125 | +### `make version` | ||
| 126 | + | ||
| 127 | +Affiche ce que ce dépôt estampillerait, sans construire. | ||
| 128 | + | ||
| 129 | +``` | ||
| 130 | +$ make version | ||
| 131 | +v0.1.0-14-g88a4c38 (88a4c38) | ||
| 132 | +``` | ||
| 133 | + | ||
| 134 | +### `make ldflags` | ||
| 135 | + | ||
| 136 | +Affiche les options d'édition de liens qu'utilise un build estampillé, pour qu'un script puisse les réutiliser au lieu de répéter les chemins `-X`. | ||
| 137 | + | ||
| 138 | +``` | ||
| 139 | +$ make ldflags | ||
| 140 | +-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' -X '….commit=7f8b36a' -X '….built=2026-08-31T19:02:03Z' | ||
| 141 | +``` | ||
| 142 | + | ||
| 143 | +`03-build-releases.sh` les lit pour ses compilations croisées, en surchargeant la version par le tag qu'il publie — `make ldflags VERSION=v0.2.0` — pour que les binaires disent ce que dit la release plutôt que ce que dit `git describe`. Un binaire compilé sans elles annonce `devel`, quoi que dise la release à laquelle il est attaché. | ||
| 144 | + | ||
| 145 | +## Voir aussi | ||
| 146 | + | ||
| 147 | +- Faire une release pour que le numéro soit juste : [Comment faire une release](../how-to/make-a-release.md) | ||
| 148 | +- Ce à quoi `-version` ne sert **pas** : il est écrit pour un humain. Un script qui a besoin du numéro doit comparer avec `grep -F`, ou interroger git, plutôt que d'en extraire un champ. | ||
| 149 | +- Pourquoi il n'y a pas de constante de version : [Décisions de conception](../explanation/design-decisions.md#la-version-est-une-propriété-du-build-pas-des-sources) | ||
| 150 | +- L'option `-version` parmi les autres : [Ligne de commande](cli.md) | ||
added
docs/fr/tutorials/getting-started.md +204 -0 | new file mode 100644 | ||
| @@ -0,0 +1,204 @@ | ||
| 1 | +# Tutoriel : votre premier programme Golo dans Turbo Golo | |
| 2 | + | |
| 3 | +À la fin de ce tutoriel, vous aurez écrit, lancé et cassé un petit programme Golo sans quitter l'éditeur — et vu l'éditeur vous dire où était l'erreur. | |
| 4 | + | |
| 5 | +Aucune connaissance préalable de Turbo Golo n'est nécessaire. Il vous faut Go 1.26 ou plus récent pour compiler l'éditeur, et l'interpréteur `golo` pour exécuter le programme. | |
| 6 | + | |
| 7 | +## Prérequis | |
| 8 | + | |
| 9 | +Vérifiez Go : | |
| 10 | + | |
| 11 | +```bash | |
| 12 | +go version | |
| 13 | +``` | |
| 14 | + | |
| 15 | +Vous devriez voir quelque chose comme : | |
| 16 | + | |
| 17 | +``` | |
| 18 | +go version go1.26.5 linux/arm64 | |
| 19 | +``` | |
| 20 | + | |
| 21 | +Vérifiez l'interpréteur : | |
| 22 | + | |
| 23 | +```bash | |
| 24 | +golo --version | |
| 25 | +``` | |
| 26 | + | |
| 27 | +``` | |
| 28 | +v0.1.1 | dev.20260802.🤓 | |
| 29 | +``` | |
| 30 | + | |
| 31 | +Si la commande est introuvable, installez-le d'abord — [un téléchargement ou un script suffit](../how-to/install-goloscript.md). | |
| 32 | + | |
| 33 | +## Étape 1 — Installer l'éditeur | |
| 34 | + | |
| 35 | +```bash | |
| 36 | +git clone https://rickub.com/turbo-editors/turbo-golo.git | |
| 37 | +cd turbo-golo | |
| 38 | +make install | |
| 39 | +``` | |
| 40 | + | |
| 41 | +L'installeur compile, installe, puis vérifie ce qu'il a installé. Les dernières lignes sont : | |
| 42 | + | |
| 43 | +``` | |
| 44 | +==> Checking the language server | |
| 45 | + ✓ golo at /usr/local/bin/golo — completion comes from `golo lsp` | |
| 46 | + | |
| 47 | +==> Ready | |
| 48 | +``` | |
| 49 | + | |
| 50 | +Nous avons maintenant une commande `turbo-golo`. | |
| 51 | + | |
| 52 | +## Étape 2 — Créer un dossier | |
| 53 | + | |
| 54 | +```bash | |
| 55 | +mkdir /tmp/hello | |
| 56 | +cd /tmp/hello | |
| 57 | +``` | |
| 58 | + | |
| 59 | +C'est là tout ce qu'il faut pour créer un projet Golo : Golo n'a pas de manifeste, un script est un fichier, et un programme est un dossier de fichiers. **Le dossier dans lequel vous démarrez l'éditeur** est celui où les commandes du menu Golo s'exécuteront, démarrez-le donc ici. | |
| 60 | + | |
| 61 | +## Étape 3 — Ouvrir un fichier qui n'existe pas encore | |
| 62 | + | |
| 63 | +```bash | |
| 64 | +turbo-golo hello.golo | |
| 65 | +``` | |
| 66 | + | |
| 67 | +L'écran se remplit. En haut : | |
| 68 | + | |
| 69 | +``` | |
| 70 | + File Edit Search Run Code Options Window Snippets Golo Help | |
| 71 | +``` | |
| 72 | + | |
| 73 | +Dix menus, et le neuvième porte le nom du langage. Au milieu, une fenêtre vide intitulée `hello.golo`. En bas, à droite, vous devriez voir : | |
| 74 | + | |
| 75 | +``` | |
| 76 | +1:1 LSP: ready | |
| 77 | +``` | |
| 78 | + | |
| 79 | +`LSP: ready` signifie que `golo lsp` — l'interpréteur, en mode serveur de langage — a démarré dans ce dossier. Nous nous en servirons à l'étape 8. | |
| 80 | + | |
| 81 | +## Étape 4 — Écrire le programme | |
| 82 | + | |
| 83 | +Tapez ceci. Tapez-le exactement ; nous allons regarder les couleurs juste après. | |
| 84 | + | |
| 85 | +```golo | |
| 86 | +module hello.World | |
| 87 | + | |
| 88 | +# Greet someone several times | |
| 89 | +struct Greeting = { name, times } | |
| 90 | + | |
| 91 | +function greet = |g| { | |
| 92 | + foreach i in range(1, g: times() + 1) { | |
| 93 | + println("Hello, " + g: name() + "! (" + i + ")") | |
| 94 | + } | |
| 95 | +} | |
| 96 | + | |
| 97 | +function main = |args| { | |
| 98 | + greet(Greeting("Golo", 3)) | |
| 99 | +} | |
| 100 | +``` | |
| 101 | + | |
| 102 | +Si une liste de complétion s'ouvre pendant que vous tapez — un `.` en demande une de lui-même, et `hello.` en est un — continuez à taper ou appuyez sur `Échap` ; `Entrée` accepterait la première entrée. | |
| 103 | + | |
| 104 | +Appuyez sur `F2` pour enregistrer. La barre d'état affiche `Saved hello.golo`. | |
| 105 | + | |
| 106 | +## Étape 5 — Lire les couleurs | |
| 107 | + | |
| 108 | +Regardez ce que vous venez de taper. Dans le thème par défaut `turbo-classic` : | |
| 109 | + | |
| 110 | +| Quoi | Couleur | | |
| 111 | +| --- | --- | | |
| 112 | +| `module`, `struct`, `function`, `foreach`, `in` | blanc vif, gras — mots-clés | | |
| 113 | +| `hello.World`, `Greeting` | cyan vif — un chemin de module, et un type | | |
| 114 | +| `greet`, là où il est déclaré comme là où il est appelé | jaune vif, gras — fonctions | | |
| 115 | +| `range`, `println` | cyan vif, gras — des fonctions fournies par l'interpréteur | | |
| 116 | +| `name`, `times`, `g`, `i`, `args` | jaune vif — noms ordinaires | | |
| 117 | +| `"Hello, "`, `"! ("`, `")"`, `"Golo"` | vert — chaînes | | |
| 118 | +| `1`, `3` | magenta — nombres | | |
| 119 | +| `# Greet someone several times` | gris — un commentaire | | |
| 120 | +| `\|`, `:`, `+`, `=` | blanc — opérateurs | | |
| 121 | + | |
| 122 | +Deux de ces lignes méritent un second regard. | |
| 123 | + | |
| 124 | +**`Greeting` est cyan et `greet` est jaune**, et personne n'a dit à l'éditeur lequel des deux est un type. C'est la convention de Golo qui tranche : les structs, les unions et leurs variantes sont les noms que l'on écrit avec une majuscule, une majuscule est donc colorée comme un type. | |
| 125 | + | |
| 126 | +**`greet` est jaune là où il est déclaré**, après `function`, bien qu'aucune parenthèse ne le suive à cet endroit. Le nom qui suit `function` est une fonction par sa position — [et c'est une règle délibérée](../explanation/colouring-and-completion.md). | |
| 127 | + | |
| 128 | +## Étape 6 — Donner ses outils au projet | |
| 129 | + | |
| 130 | +Appuyez sur `F10` pour ouvrir la barre de menus, puis sur `→` **huit fois** pour atteindre **Golo** — après Edit, Search, Run, Code, Options, Window et Snippets. Plus rapide : `Alt-G`. | |
| 131 | + | |
| 132 | +Le menu contient deux entrées, et une seule est disponible : | |
| 133 | + | |
| 134 | +``` | |
| 135 | +┌───────────────────┐ | |
| 136 | +│ Create tools file │ | |
| 137 | +│ Open tools file │ ← grisée ; il n'y a pas encore de fichier à ouvrir | |
| 138 | +└───────────────────┘ | |
| 139 | +``` | |
| 140 | + | |
| 141 | +Choisissez **Create tools file**. | |
| 142 | + | |
| 143 | +Une seconde fenêtre s'ouvre sur le fichier qui vient d'être écrit, `.turbo-golo/tools.toml`. Lisez-le si vous voulez — il explique chacune de ses clés — puis fermez-le avec `Ctrl-W`. | |
| 144 | + | |
| 145 | +Regardez la barre de menus : un menu **Tools** est apparu entre Golo et Help. La dernière entrée du fichier de départ nomme un menu à elle, et cette seule ligne est tout le mécanisme. | |
| 146 | + | |
| 147 | +Rouvrez le menu Golo. Il contient maintenant huit commandes, et les deux entrées ont échangé leurs rôles : `Create tools file` est grisée, et c'est `Open tools file` qui est choisissable. | |
| 148 | + | |
| 149 | +## Étape 7 — Le lancer | |
| 150 | + | |
| 151 | +Appuyez sur `Alt-G` et choisissez **Run**. Une boîte demande une valeur avant de lancer la commande, parce que Golo n'a pas de manifeste qui dise quel fichier est le programme : | |
| 152 | + | |
| 153 | +``` | |
| 154 | +┌──────────────── Run ────────────────┐ | |
| 155 | +│ script, e.g. main.golo │ | |
| 156 | +│ [ ] │ | |
| 157 | +└─────────────────────────────────────┘ | |
| 158 | +``` | |
| 159 | + | |
| 160 | +Tapez `hello.golo` et appuyez sur `Entrée`. Une fenêtre de terminal s'ouvre et le programme s'y exécute : | |
| 161 | + | |
| 162 | +``` | |
| 163 | +Hello, Golo! (1) | |
| 164 | +Hello, Golo! (2) | |
| 165 | +Hello, Golo! (3) | |
| 166 | +``` | |
| 167 | + | |
| 168 | +Un terminal plutôt qu'une boîte de dialogue, parce qu'un programme qui lit le clavier doit pouvoir recevoir une réponse. Le programme est terminé, la fenêtre a donc cessé de se comporter comme un terminal et toutes les touches reviennent à l'éditeur : appuyez sur `Ctrl-W` pour la fermer. | |
| 169 | + | |
| 170 | +## Étape 8 — Le casser, et voir où | |
| 171 | + | |
| 172 | +Allez à la fin du fichier, après la dernière `}`, et ajoutez un commentaire comme un autre langage l'écrirait : | |
| 173 | + | |
| 174 | +```golo | |
| 175 | +// run it | |
| 176 | +``` | |
| 177 | + | |
| 178 | +Appuyez sur `F2` pour enregistrer. | |
| 179 | + | |
| 180 | +En moins d'une seconde, deux choses se produisent. Un `×` rouge apparaît dans la gouttière, juste à gauche du numéro de cette ligne. Et la barre d'état affiche : | |
| 181 | + | |
| 182 | +``` | |
| 183 | +⚠ GoloScript has no '//' line comments. GoloScript comments are '#' for a single line (e.g. … | |
| 184 | +``` | |
| 185 | + | |
| 186 | +Personne ne l'a demandé. `golo lsp` le publie de lui-même chaque fois qu'il relit le fichier — une erreur de syntaxe aurait valu la même marque, et celle-ci est un lint que le serveur ajoute parce que c'est la première erreur que fait quiconque arrive d'un autre langage. | |
| 187 | + | |
| 188 | +Remplacez le `//` par `#` et enregistrez de nouveau ; la marque et le message disparaissent tous les deux. | |
| 189 | + | |
| 190 | +## Étape 9 — Changer de thème | |
| 191 | + | |
| 192 | +`F10`, puis `→` **cinq fois** pour atteindre **Options** — après Edit, Search, Run et Code. Choisissez **Theme…**. | |
| 193 | + | |
| 194 | +Une liste s'ouvre sur le thème dans lequel vous êtes. Appuyez sur `↓` jusqu'à `cobalt` puis `Entrée`. Tout l'écran change, en gardant la même forme. | |
| 195 | + | |
| 196 | +Appuyez sur `Alt-X` pour quitter. L'éditeur pose d'abord la question des fichiers non enregistrés, s'il y en a. | |
| 197 | + | |
| 198 | +## Et maintenant ? | |
| 199 | + | |
| 200 | +Vous avez écrit un programme Golo dans l'éditeur, l'avez lancé, cassé, et vu l'éditeur dire où. Pour aller plus loin : | |
| 201 | + | |
| 202 | +- Pour faire des choses précises → les [guides pratiques](../how-to/) | |
| 203 | +- Pour savoir exactement ce qui est coloré et comment → [langages colorés](../reference/languages.md) | |
| 204 | +- Pour comprendre pourquoi l'éditeur est bâti ainsi → les [explications](../explanation/) | |
| new file mode 100644 | |||
| @@ -0,0 +1,204 @@ | |||
| 1 | +# Tutoriel : votre premier programme Golo dans Turbo Golo | ||
| 2 | + | ||
| 3 | +À la fin de ce tutoriel, vous aurez écrit, lancé et cassé un petit programme Golo sans quitter l'éditeur — et vu l'éditeur vous dire où était l'erreur. | ||
| 4 | + | ||
| 5 | +Aucune connaissance préalable de Turbo Golo n'est nécessaire. Il vous faut Go 1.26 ou plus récent pour compiler l'éditeur, et l'interpréteur `golo` pour exécuter le programme. | ||
| 6 | + | ||
| 7 | +## Prérequis | ||
| 8 | + | ||
| 9 | +Vérifiez Go : | ||
| 10 | + | ||
| 11 | +```bash | ||
| 12 | +go version | ||
| 13 | +``` | ||
| 14 | + | ||
| 15 | +Vous devriez voir quelque chose comme : | ||
| 16 | + | ||
| 17 | +``` | ||
| 18 | +go version go1.26.5 linux/arm64 | ||
| 19 | +``` | ||
| 20 | + | ||
| 21 | +Vérifiez l'interpréteur : | ||
| 22 | + | ||
| 23 | +```bash | ||
| 24 | +golo --version | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +``` | ||
| 28 | +v0.1.1 | dev.20260802.🤓 | ||
| 29 | +``` | ||
| 30 | + | ||
| 31 | +Si la commande est introuvable, installez-le d'abord — [un téléchargement ou un script suffit](../how-to/install-goloscript.md). | ||
| 32 | + | ||
| 33 | +## Étape 1 — Installer l'éditeur | ||
| 34 | + | ||
| 35 | +```bash | ||
| 36 | +git clone https://rickub.com/turbo-editors/turbo-golo.git | ||
| 37 | +cd turbo-golo | ||
| 38 | +make install | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +L'installeur compile, installe, puis vérifie ce qu'il a installé. Les dernières lignes sont : | ||
| 42 | + | ||
| 43 | +``` | ||
| 44 | +==> Checking the language server | ||
| 45 | + ✓ golo at /usr/local/bin/golo — completion comes from `golo lsp` | ||
| 46 | + | ||
| 47 | +==> Ready | ||
| 48 | +``` | ||
| 49 | + | ||
| 50 | +Nous avons maintenant une commande `turbo-golo`. | ||
| 51 | + | ||
| 52 | +## Étape 2 — Créer un dossier | ||
| 53 | + | ||
| 54 | +```bash | ||
| 55 | +mkdir /tmp/hello | ||
| 56 | +cd /tmp/hello | ||
| 57 | +``` | ||
| 58 | + | ||
| 59 | +C'est là tout ce qu'il faut pour créer un projet Golo : Golo n'a pas de manifeste, un script est un fichier, et un programme est un dossier de fichiers. **Le dossier dans lequel vous démarrez l'éditeur** est celui où les commandes du menu Golo s'exécuteront, démarrez-le donc ici. | ||
| 60 | + | ||
| 61 | +## Étape 3 — Ouvrir un fichier qui n'existe pas encore | ||
| 62 | + | ||
| 63 | +```bash | ||
| 64 | +turbo-golo hello.golo | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +L'écran se remplit. En haut : | ||
| 68 | + | ||
| 69 | +``` | ||
| 70 | + File Edit Search Run Code Options Window Snippets Golo Help | ||
| 71 | +``` | ||
| 72 | + | ||
| 73 | +Dix menus, et le neuvième porte le nom du langage. Au milieu, une fenêtre vide intitulée `hello.golo`. En bas, à droite, vous devriez voir : | ||
| 74 | + | ||
| 75 | +``` | ||
| 76 | +1:1 LSP: ready | ||
| 77 | +``` | ||
| 78 | + | ||
| 79 | +`LSP: ready` signifie que `golo lsp` — l'interpréteur, en mode serveur de langage — a démarré dans ce dossier. Nous nous en servirons à l'étape 8. | ||
| 80 | + | ||
| 81 | +## Étape 4 — Écrire le programme | ||
| 82 | + | ||
| 83 | +Tapez ceci. Tapez-le exactement ; nous allons regarder les couleurs juste après. | ||
| 84 | + | ||
| 85 | +```golo | ||
| 86 | +module hello.World | ||
| 87 | + | ||
| 88 | +# Greet someone several times | ||
| 89 | +struct Greeting = { name, times } | ||
| 90 | + | ||
| 91 | +function greet = |g| { | ||
| 92 | + foreach i in range(1, g: times() + 1) { | ||
| 93 | + println("Hello, " + g: name() + "! (" + i + ")") | ||
| 94 | + } | ||
| 95 | +} | ||
| 96 | + | ||
| 97 | +function main = |args| { | ||
| 98 | + greet(Greeting("Golo", 3)) | ||
| 99 | +} | ||
| 100 | +``` | ||
| 101 | + | ||
| 102 | +Si une liste de complétion s'ouvre pendant que vous tapez — un `.` en demande une de lui-même, et `hello.` en est un — continuez à taper ou appuyez sur `Échap` ; `Entrée` accepterait la première entrée. | ||
| 103 | + | ||
| 104 | +Appuyez sur `F2` pour enregistrer. La barre d'état affiche `Saved hello.golo`. | ||
| 105 | + | ||
| 106 | +## Étape 5 — Lire les couleurs | ||
| 107 | + | ||
| 108 | +Regardez ce que vous venez de taper. Dans le thème par défaut `turbo-classic` : | ||
| 109 | + | ||
| 110 | +| Quoi | Couleur | | ||
| 111 | +| --- | --- | | ||
| 112 | +| `module`, `struct`, `function`, `foreach`, `in` | blanc vif, gras — mots-clés | | ||
| 113 | +| `hello.World`, `Greeting` | cyan vif — un chemin de module, et un type | | ||
| 114 | +| `greet`, là où il est déclaré comme là où il est appelé | jaune vif, gras — fonctions | | ||
| 115 | +| `range`, `println` | cyan vif, gras — des fonctions fournies par l'interpréteur | | ||
| 116 | +| `name`, `times`, `g`, `i`, `args` | jaune vif — noms ordinaires | | ||
| 117 | +| `"Hello, "`, `"! ("`, `")"`, `"Golo"` | vert — chaînes | | ||
| 118 | +| `1`, `3` | magenta — nombres | | ||
| 119 | +| `# Greet someone several times` | gris — un commentaire | | ||
| 120 | +| `\|`, `:`, `+`, `=` | blanc — opérateurs | | ||
| 121 | + | ||
| 122 | +Deux de ces lignes méritent un second regard. | ||
| 123 | + | ||
| 124 | +**`Greeting` est cyan et `greet` est jaune**, et personne n'a dit à l'éditeur lequel des deux est un type. C'est la convention de Golo qui tranche : les structs, les unions et leurs variantes sont les noms que l'on écrit avec une majuscule, une majuscule est donc colorée comme un type. | ||
| 125 | + | ||
| 126 | +**`greet` est jaune là où il est déclaré**, après `function`, bien qu'aucune parenthèse ne le suive à cet endroit. Le nom qui suit `function` est une fonction par sa position — [et c'est une règle délibérée](../explanation/colouring-and-completion.md). | ||
| 127 | + | ||
| 128 | +## Étape 6 — Donner ses outils au projet | ||
| 129 | + | ||
| 130 | +Appuyez sur `F10` pour ouvrir la barre de menus, puis sur `→` **huit fois** pour atteindre **Golo** — après Edit, Search, Run, Code, Options, Window et Snippets. Plus rapide : `Alt-G`. | ||
| 131 | + | ||
| 132 | +Le menu contient deux entrées, et une seule est disponible : | ||
| 133 | + | ||
| 134 | +``` | ||
| 135 | +┌───────────────────┐ | ||
| 136 | +│ Create tools file │ | ||
| 137 | +│ Open tools file │ ← grisée ; il n'y a pas encore de fichier à ouvrir | ||
| 138 | +└───────────────────┘ | ||
| 139 | +``` | ||
| 140 | + | ||
| 141 | +Choisissez **Create tools file**. | ||
| 142 | + | ||
| 143 | +Une seconde fenêtre s'ouvre sur le fichier qui vient d'être écrit, `.turbo-golo/tools.toml`. Lisez-le si vous voulez — il explique chacune de ses clés — puis fermez-le avec `Ctrl-W`. | ||
| 144 | + | ||
| 145 | +Regardez la barre de menus : un menu **Tools** est apparu entre Golo et Help. La dernière entrée du fichier de départ nomme un menu à elle, et cette seule ligne est tout le mécanisme. | ||
| 146 | + | ||
| 147 | +Rouvrez le menu Golo. Il contient maintenant huit commandes, et les deux entrées ont échangé leurs rôles : `Create tools file` est grisée, et c'est `Open tools file` qui est choisissable. | ||
| 148 | + | ||
| 149 | +## Étape 7 — Le lancer | ||
| 150 | + | ||
| 151 | +Appuyez sur `Alt-G` et choisissez **Run**. Une boîte demande une valeur avant de lancer la commande, parce que Golo n'a pas de manifeste qui dise quel fichier est le programme : | ||
| 152 | + | ||
| 153 | +``` | ||
| 154 | +┌──────────────── Run ────────────────┐ | ||
| 155 | +│ script, e.g. main.golo │ | ||
| 156 | +│ [ ] │ | ||
| 157 | +└─────────────────────────────────────┘ | ||
| 158 | +``` | ||
| 159 | + | ||
| 160 | +Tapez `hello.golo` et appuyez sur `Entrée`. Une fenêtre de terminal s'ouvre et le programme s'y exécute : | ||
| 161 | + | ||
| 162 | +``` | ||
| 163 | +Hello, Golo! (1) | ||
| 164 | +Hello, Golo! (2) | ||
| 165 | +Hello, Golo! (3) | ||
| 166 | +``` | ||
| 167 | + | ||
| 168 | +Un terminal plutôt qu'une boîte de dialogue, parce qu'un programme qui lit le clavier doit pouvoir recevoir une réponse. Le programme est terminé, la fenêtre a donc cessé de se comporter comme un terminal et toutes les touches reviennent à l'éditeur : appuyez sur `Ctrl-W` pour la fermer. | ||
| 169 | + | ||
| 170 | +## Étape 8 — Le casser, et voir où | ||
| 171 | + | ||
| 172 | +Allez à la fin du fichier, après la dernière `}`, et ajoutez un commentaire comme un autre langage l'écrirait : | ||
| 173 | + | ||
| 174 | +```golo | ||
| 175 | +// run it | ||
| 176 | +``` | ||
| 177 | + | ||
| 178 | +Appuyez sur `F2` pour enregistrer. | ||
| 179 | + | ||
| 180 | +En moins d'une seconde, deux choses se produisent. Un `×` rouge apparaît dans la gouttière, juste à gauche du numéro de cette ligne. Et la barre d'état affiche : | ||
| 181 | + | ||
| 182 | +``` | ||
| 183 | +⚠ GoloScript has no '//' line comments. GoloScript comments are '#' for a single line (e.g. … | ||
| 184 | +``` | ||
| 185 | + | ||
| 186 | +Personne ne l'a demandé. `golo lsp` le publie de lui-même chaque fois qu'il relit le fichier — une erreur de syntaxe aurait valu la même marque, et celle-ci est un lint que le serveur ajoute parce que c'est la première erreur que fait quiconque arrive d'un autre langage. | ||
| 187 | + | ||
| 188 | +Remplacez le `//` par `#` et enregistrez de nouveau ; la marque et le message disparaissent tous les deux. | ||
| 189 | + | ||
| 190 | +## Étape 9 — Changer de thème | ||
| 191 | + | ||
| 192 | +`F10`, puis `→` **cinq fois** pour atteindre **Options** — après Edit, Search, Run et Code. Choisissez **Theme…**. | ||
| 193 | + | ||
| 194 | +Une liste s'ouvre sur le thème dans lequel vous êtes. Appuyez sur `↓` jusqu'à `cobalt` puis `Entrée`. Tout l'écran change, en gardant la même forme. | ||
| 195 | + | ||
| 196 | +Appuyez sur `Alt-X` pour quitter. L'éditeur pose d'abord la question des fichiers non enregistrés, s'il y en a. | ||
| 197 | + | ||
| 198 | +## Et maintenant ? | ||
| 199 | + | ||
| 200 | +Vous avez écrit un programme Golo dans l'éditeur, l'avez lancé, cassé, et vu l'éditeur dire où. Pour aller plus loin : | ||
| 201 | + | ||
| 202 | +- Pour faire des choses précises → les [guides pratiques](../how-to/) | ||
| 203 | +- Pour savoir exactement ce qui est coloré et comment → [langages colorés](../reference/languages.md) | ||
| 204 | +- Pour comprendre pourquoi l'éditeur est bâti ainsi → les [explications](../explanation/) | ||
added
git.sh +136 -0 | new file mode 100755 | ||
| @@ -0,0 +1,136 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +message="" | |
| 3 | +case $1 in | |
| 4 | + | |
| 5 | + # 🎨: art | |
| 6 | + art) | |
| 7 | + message="Improve structure / format of the code" | |
| 8 | + emoji="🎨" | |
| 9 | + ;; | |
| 10 | + | |
| 11 | + # 🐛: bug | |
| 12 | + bug|fix) | |
| 13 | + message="Fix a bug" | |
| 14 | + emoji="🐛" | |
| 15 | + ;; | |
| 16 | + | |
| 17 | + # 🤓: geek | |
| 18 | + human|human-fixed) | |
| 19 | + message="Human Fixed" | |
| 20 | + emoji="🤓" | |
| 21 | + ;; | |
| 22 | + | |
| 23 | + # 🤖: robot | |
| 24 | + ai|ai-generated) | |
| 25 | + message="AI generated" | |
| 26 | + emoji="🤖" | |
| 27 | + ;; | |
| 28 | + | |
| 29 | + # ✨: sparkles | |
| 30 | + sparkles|feature) | |
| 31 | + message="Introduce new feature(s)" | |
| 32 | + emoji="✨" | |
| 33 | + ;; | |
| 34 | + | |
| 35 | + # 🧩: jigsaw | |
| 36 | + jigsaw|example|examples|demo|demos) | |
| 37 | + message="Introduce new example(s)" | |
| 38 | + emoji="🧩" | |
| 39 | + ;; | |
| 40 | + | |
| 41 | + | |
| 42 | + # 📝: memo | |
| 43 | + memo|doc|documentation) | |
| 44 | + message="Add or update documentation" | |
| 45 | + emoji="📝" | |
| 46 | + ;; | |
| 47 | + | |
| 48 | + # 🌸: cherry_blossom | |
| 49 | + gardening|garden|clean|cleaning) | |
| 50 | + message="Gardening" | |
| 51 | + emoji="🌸" | |
| 52 | + ;; | |
| 53 | + | |
| 54 | + # 🚀: rocket | |
| 55 | + rocket|deploy) | |
| 56 | + message="Deploy stuff" | |
| 57 | + emoji="🚀" | |
| 58 | + ;; | |
| 59 | + | |
| 60 | + # 🎉: tada | |
| 61 | + tada|first) | |
| 62 | + message="Begin a project" | |
| 63 | + emoji="🎉" | |
| 64 | + ;; | |
| 65 | + | |
| 66 | + # 🚧: construction | |
| 67 | + construction|wip) | |
| 68 | + message="Work in progress" | |
| 69 | + emoji="🚧" | |
| 70 | + ;; | |
| 71 | + | |
| 72 | + # 📦️: package | |
| 73 | + package|build) | |
| 74 | + message="Add or update compiled files or packages" | |
| 75 | + emoji="📦️" | |
| 76 | + ;; | |
| 77 | + | |
| 78 | + # 📦️: package | |
| 79 | + release) | |
| 80 | + message="Create a release" | |
| 81 | + emoji="📦️" | |
| 82 | + ;; | |
| 83 | + | |
| 84 | + # 👽️: alien | |
| 85 | + alien|api) | |
| 86 | + message="Update code due to external API changes" | |
| 87 | + emoji="👽️" | |
| 88 | + ;; | |
| 89 | + | |
| 90 | + # 🐳: whale | |
| 91 | + docker|container) | |
| 92 | + message="Docker" | |
| 93 | + emoji="🐳" | |
| 94 | + ;; | |
| 95 | + | |
| 96 | + # 🍊: tangerine | |
| 97 | + gitpod|gitpodify) | |
| 98 | + message="Gitpodify" | |
| 99 | + emoji="🍊" | |
| 100 | + ;; | |
| 101 | + | |
| 102 | + # 🧪: test tube | |
| 103 | + alembic|experiments|experiment|xp) | |
| 104 | + message="Perform experiments" | |
| 105 | + emoji="🧪" | |
| 106 | + ;; | |
| 107 | + | |
| 108 | + # ✅: check mark | |
| 109 | + test|tests|testing) | |
| 110 | + message="Add or update tests" | |
| 111 | + emoji="✅" | |
| 112 | + ;; | |
| 113 | + | |
| 114 | + # 💾: floppy-disk | |
| 115 | + save) | |
| 116 | + message="Saved" | |
| 117 | + emoji="💾" | |
| 118 | + ;; | |
| 119 | + | |
| 120 | + *) | |
| 121 | + message="Updated" | |
| 122 | + emoji="🛟" | |
| 123 | + ;; | |
| 124 | + | |
| 125 | +esac | |
| 126 | + | |
| 127 | +find . -name '.DS_Store' -type f -delete | |
| 128 | + | |
| 129 | +if [ -z "$2" ] | |
| 130 | +then | |
| 131 | + # empty | |
| 132 | + git add .; git commit -m "$emoji $message."; git push | |
| 133 | +else | |
| 134 | + # not empty | |
| 135 | + git add .; git commit -m "$emoji $message: $2"; git push | |
| 136 | +fi | |
| new file mode 100755 | |||
| @@ -0,0 +1,136 @@ | |||
| 1 | +#!/bin/bash | ||
| 2 | +message="" | ||
| 3 | +case $1 in | ||
| 4 | + | ||
| 5 | + # 🎨: art | ||
| 6 | + art) | ||
| 7 | + message="Improve structure / format of the code" | ||
| 8 | + emoji="🎨" | ||
| 9 | + ;; | ||
| 10 | + | ||
| 11 | + # 🐛: bug | ||
| 12 | + bug|fix) | ||
| 13 | + message="Fix a bug" | ||
| 14 | + emoji="🐛" | ||
| 15 | + ;; | ||
| 16 | + | ||
| 17 | + # 🤓: geek | ||
| 18 | + human|human-fixed) | ||
| 19 | + message="Human Fixed" | ||
| 20 | + emoji="🤓" | ||
| 21 | + ;; | ||
| 22 | + | ||
| 23 | + # 🤖: robot | ||
| 24 | + ai|ai-generated) | ||
| 25 | + message="AI generated" | ||
| 26 | + emoji="🤖" | ||
| 27 | + ;; | ||
| 28 | + | ||
| 29 | + # ✨: sparkles | ||
| 30 | + sparkles|feature) | ||
| 31 | + message="Introduce new feature(s)" | ||
| 32 | + emoji="✨" | ||
| 33 | + ;; | ||
| 34 | + | ||
| 35 | + # 🧩: jigsaw | ||
| 36 | + jigsaw|example|examples|demo|demos) | ||
| 37 | + message="Introduce new example(s)" | ||
| 38 | + emoji="🧩" | ||
| 39 | + ;; | ||
| 40 | + | ||
| 41 | + | ||
| 42 | + # 📝: memo | ||
| 43 | + memo|doc|documentation) | ||
| 44 | + message="Add or update documentation" | ||
| 45 | + emoji="📝" | ||
| 46 | + ;; | ||
| 47 | + | ||
| 48 | + # 🌸: cherry_blossom | ||
| 49 | + gardening|garden|clean|cleaning) | ||
| 50 | + message="Gardening" | ||
| 51 | + emoji="🌸" | ||
| 52 | + ;; | ||
| 53 | + | ||
| 54 | + # 🚀: rocket | ||
| 55 | + rocket|deploy) | ||
| 56 | + message="Deploy stuff" | ||
| 57 | + emoji="🚀" | ||
| 58 | + ;; | ||
| 59 | + | ||
| 60 | + # 🎉: tada | ||
| 61 | + tada|first) | ||
| 62 | + message="Begin a project" | ||
| 63 | + emoji="🎉" | ||
| 64 | + ;; | ||
| 65 | + | ||
| 66 | + # 🚧: construction | ||
| 67 | + construction|wip) | ||
| 68 | + message="Work in progress" | ||
| 69 | + emoji="🚧" | ||
| 70 | + ;; | ||
| 71 | + | ||
| 72 | + # 📦️: package | ||
| 73 | + package|build) | ||
| 74 | + message="Add or update compiled files or packages" | ||
| 75 | + emoji="📦️" | ||
| 76 | + ;; | ||
| 77 | + | ||
| 78 | + # 📦️: package | ||
| 79 | + release) | ||
| 80 | + message="Create a release" | ||
| 81 | + emoji="📦️" | ||
| 82 | + ;; | ||
| 83 | + | ||
| 84 | + # 👽️: alien | ||
| 85 | + alien|api) | ||
| 86 | + message="Update code due to external API changes" | ||
| 87 | + emoji="👽️" | ||
| 88 | + ;; | ||
| 89 | + | ||
| 90 | + # 🐳: whale | ||
| 91 | + docker|container) | ||
| 92 | + message="Docker" | ||
| 93 | + emoji="🐳" | ||
| 94 | + ;; | ||
| 95 | + | ||
| 96 | + # 🍊: tangerine | ||
| 97 | + gitpod|gitpodify) | ||
| 98 | + message="Gitpodify" | ||
| 99 | + emoji="🍊" | ||
| 100 | + ;; | ||
| 101 | + | ||
| 102 | + # 🧪: test tube | ||
| 103 | + alembic|experiments|experiment|xp) | ||
| 104 | + message="Perform experiments" | ||
| 105 | + emoji="🧪" | ||
| 106 | + ;; | ||
| 107 | + | ||
| 108 | + # ✅: check mark | ||
| 109 | + test|tests|testing) | ||
| 110 | + message="Add or update tests" | ||
| 111 | + emoji="✅" | ||
| 112 | + ;; | ||
| 113 | + | ||
| 114 | + # 💾: floppy-disk | ||
| 115 | + save) | ||
| 116 | + message="Saved" | ||
| 117 | + emoji="💾" | ||
| 118 | + ;; | ||
| 119 | + | ||
| 120 | + *) | ||
| 121 | + message="Updated" | ||
| 122 | + emoji="🛟" | ||
| 123 | + ;; | ||
| 124 | + | ||
| 125 | +esac | ||
| 126 | + | ||
| 127 | +find . -name '.DS_Store' -type f -delete | ||
| 128 | + | ||
| 129 | +if [ -z "$2" ] | ||
| 130 | +then | ||
| 131 | + # empty | ||
| 132 | + git add .; git commit -m "$emoji $message."; git push | ||
| 133 | +else | ||
| 134 | + # not empty | ||
| 135 | + git add .; git commit -m "$emoji $message: $2"; git push | ||
| 136 | +fi | ||
added
go.mod +23 -0 | new file mode 100644 | ||
| @@ -0,0 +1,23 @@ | ||
| 1 | +module rickub.com/turbo-editors/turbo-golo | |
| 2 | + | |
| 3 | +go 1.26.1 | |
| 4 | + | |
| 5 | +require ( | |
| 6 | + github.com/gdamore/tcell/v2 v2.13.10 | |
| 7 | + rickub.com/turbo-editors/turbo-core v1.0.0 | |
| 8 | +) | |
| 9 | + | |
| 10 | +require ( | |
| 11 | + github.com/BurntSushi/toml v1.6.0 // indirect | |
| 12 | + github.com/gdamore/encoding v1.0.1 // indirect | |
| 13 | + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect | |
| 14 | + github.com/rivo/uniseg v0.4.7 // indirect | |
| 15 | + golang.org/x/sys v0.38.0 // indirect | |
| 16 | + golang.org/x/term v0.37.0 // indirect | |
| 17 | + golang.org/x/text v0.31.0 // indirect | |
| 18 | +) | |
| 19 | + | |
| 20 | +// turbo-core is developed alongside the editors that use it. Point this at the | |
| 21 | +// checkout beside this one so the whole family builds from a clean clone of the | |
| 22 | +// three repositories; drop it once the version above is tagged and published. | |
| 23 | +// replace rickub.com/turbo-editors/turbo-core => ../turbo-core | |
| new file mode 100644 | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | +module rickub.com/turbo-editors/turbo-golo | ||
| 2 | + | ||
| 3 | +go 1.26.1 | ||
| 4 | + | ||
| 5 | +require ( | ||
| 6 | + github.com/gdamore/tcell/v2 v2.13.10 | ||
| 7 | + rickub.com/turbo-editors/turbo-core v1.0.0 | ||
| 8 | +) | ||
| 9 | + | ||
| 10 | +require ( | ||
| 11 | + github.com/BurntSushi/toml v1.6.0 // indirect | ||
| 12 | + github.com/gdamore/encoding v1.0.1 // indirect | ||
| 13 | + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect | ||
| 14 | + github.com/rivo/uniseg v0.4.7 // indirect | ||
| 15 | + golang.org/x/sys v0.38.0 // indirect | ||
| 16 | + golang.org/x/term v0.37.0 // indirect | ||
| 17 | + golang.org/x/text v0.31.0 // indirect | ||
| 18 | +) | ||
| 19 | + | ||
| 20 | +// turbo-core is developed alongside the editors that use it. Point this at the | ||
| 21 | +// checkout beside this one so the whole family builds from a clean clone of the | ||
| 22 | +// three repositories; drop it once the version above is tagged and published. | ||
| 23 | +// replace rickub.com/turbo-editors/turbo-core => ../turbo-core | ||
added
go.sum +49 -0 | new file mode 100644 | ||
| @@ -0,0 +1,49 @@ | ||
| 1 | +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= | |
| 2 | +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= | |
| 3 | +github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= | |
| 4 | +github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= | |
| 5 | +github.com/gdamore/tcell/v2 v2.13.10 h1:Afs3JKt83HnhuUKdZ3MnxUgOqQRWftj5JyDqv1LLynA= | |
| 6 | +github.com/gdamore/tcell/v2 v2.13.10/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= | |
| 7 | +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= | |
| 8 | +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | |
| 9 | +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | |
| 10 | +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | |
| 11 | +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | |
| 12 | +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | |
| 13 | +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | |
| 14 | +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | |
| 15 | +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | |
| 16 | +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |
| 17 | +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | |
| 18 | +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | |
| 19 | +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | |
| 20 | +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
| 21 | +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
| 22 | +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
| 23 | +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |
| 24 | +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
| 25 | +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
| 26 | +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
| 27 | +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
| 28 | +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
| 29 | +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= | |
| 30 | +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | |
| 31 | +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | |
| 32 | +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | |
| 33 | +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | |
| 34 | +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= | |
| 35 | +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= | |
| 36 | +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |
| 37 | +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | |
| 38 | +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | |
| 39 | +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | |
| 40 | +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | |
| 41 | +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= | |
| 42 | +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= | |
| 43 | +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | |
| 44 | +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | |
| 45 | +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | |
| 46 | +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | |
| 47 | +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
| 48 | +rickub.com/turbo-editors/turbo-core v1.0.0 h1:+tzwwONYXO46o+JpWHFR8PsGHTPLKtgd5NLZx9g3cdY= | |
| 49 | +rickub.com/turbo-editors/turbo-core v1.0.0/go.mod h1:rmfIY5gsFEo3sC5IIJFapwsGvdKG6NjrD7ACmnNTKq8= | |
| new file mode 100644 | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= | ||
| 2 | +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= | ||
| 3 | +github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= | ||
| 4 | +github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= | ||
| 5 | +github.com/gdamore/tcell/v2 v2.13.10 h1:Afs3JKt83HnhuUKdZ3MnxUgOqQRWftj5JyDqv1LLynA= | ||
| 6 | +github.com/gdamore/tcell/v2 v2.13.10/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= | ||
| 7 | +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= | ||
| 8 | +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | ||
| 9 | +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | ||
| 10 | +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | ||
| 11 | +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||
| 12 | +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||
| 13 | +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||
| 14 | +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||
| 15 | +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||
| 16 | +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||
| 17 | +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||
| 18 | +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||
| 19 | +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||
| 20 | +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
| 21 | +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
| 22 | +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
| 23 | +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
| 24 | +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
| 25 | +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| 26 | +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| 27 | +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| 28 | +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| 29 | +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= | ||
| 30 | +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | ||
| 31 | +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||
| 32 | +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||
| 33 | +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | ||
| 34 | +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= | ||
| 35 | +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= | ||
| 36 | +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
| 37 | +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||
| 38 | +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||
| 39 | +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||
| 40 | +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||
| 41 | +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= | ||
| 42 | +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= | ||
| 43 | +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||
| 44 | +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||
| 45 | +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||
| 46 | +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | ||
| 47 | +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||
| 48 | +rickub.com/turbo-editors/turbo-core v1.0.0 h1:+tzwwONYXO46o+JpWHFR8PsGHTPLKtgd5NLZx9g3cdY= | ||
| 49 | +rickub.com/turbo-editors/turbo-core v1.0.0/go.mod h1:rmfIY5gsFEo3sC5IIJFapwsGvdKG6NjrD7ACmnNTKq8= | ||
added
install_test.go +381 -0 | new file mode 100644 | ||
| @@ -0,0 +1,381 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "os" | |
| 5 | + "os/exec" | |
| 6 | + "path/filepath" | |
| 7 | + "runtime" | |
| 8 | + "strings" | |
| 9 | + "syscall" | |
| 10 | + "testing" | |
| 11 | +) | |
| 12 | + | |
| 13 | +// runInstaller runs scripts/install.sh with the given arguments and returns | |
| 14 | +// everything it printed, failing the test if it did not exit cleanly. | |
| 15 | +func runInstaller(t *testing.T, args ...string) string { | |
| 16 | + t.Helper() | |
| 17 | + | |
| 18 | + output, err := exec.Command("bash", append([]string{"scripts/install.sh"}, args...)...).CombinedOutput() | |
| 19 | + if err != nil { | |
| 20 | + t.Fatalf("scripts/install.sh %v failed: %v\n%s", args, err, output) | |
| 21 | + } | |
| 22 | + return string(output) | |
| 23 | +} | |
| 24 | + | |
| 25 | +// skipUnlessShellIsAvailable skips a test where the installer cannot run. | |
| 26 | +func skipUnlessShellIsAvailable(t *testing.T) { | |
| 27 | + t.Helper() | |
| 28 | + | |
| 29 | + if testing.Short() { | |
| 30 | + t.Skip("-short: the installer compiles the whole editor") | |
| 31 | + } | |
| 32 | + if runtime.GOOS == "windows" { | |
| 33 | + t.Skip("the installer is a shell script") | |
| 34 | + } | |
| 35 | + if _, err := exec.LookPath("bash"); err != nil { | |
| 36 | + t.Skip("bash is not available") | |
| 37 | + } | |
| 38 | +} | |
| 39 | + | |
| 40 | +func TestTheInstallerBuildsAWorkingBinary(t *testing.T) { | |
| 41 | + skipUnlessShellIsAvailable(t) | |
| 42 | + prefix := t.TempDir() | |
| 43 | + | |
| 44 | + output := runInstaller(t, "--prefix", prefix) | |
| 45 | + | |
| 46 | + binary := filepath.Join(prefix, "turbo-golo") | |
| 47 | + info, err := os.Stat(binary) | |
| 48 | + if err != nil { | |
| 49 | + t.Fatalf("nothing was installed at %s: %v\n%s", binary, err, output) | |
| 50 | + } | |
| 51 | + if info.Mode().Perm()&0o111 == 0 { | |
| 52 | + t.Errorf("the installed file has permissions %o, want it executable", info.Mode().Perm()) | |
| 53 | + } | |
| 54 | + | |
| 55 | + version, err := exec.Command(binary, "-version").Output() | |
| 56 | + if err != nil { | |
| 57 | + t.Fatalf("the installed binary does not run: %v", err) | |
| 58 | + } | |
| 59 | + if !strings.Contains(string(version), "Turbo Golo") { | |
| 60 | + t.Errorf("-version printed %q", version) | |
| 61 | + } | |
| 62 | +} | |
| 63 | + | |
| 64 | +func TestTheInstallerSaysWhereItPutThings(t *testing.T) { | |
| 65 | + skipUnlessShellIsAvailable(t) | |
| 66 | + prefix := t.TempDir() | |
| 67 | + | |
| 68 | + output := runInstaller(t, "--prefix", prefix) | |
| 69 | + | |
| 70 | + for _, want := range []string{"Turbo Golo", prefix, "PATH", "golo"} { | |
| 71 | + if !strings.Contains(output, want) { | |
| 72 | + t.Errorf("the installer never mentions %q:\n%s", want, output) | |
| 73 | + } | |
| 74 | + } | |
| 75 | +} | |
| 76 | + | |
| 77 | +func TestTheInstallerWarnsWhenThePrefixIsNotOnPath(t *testing.T) { | |
| 78 | + skipUnlessShellIsAvailable(t) | |
| 79 | + prefix := t.TempDir() // a fresh temporary directory is never on PATH | |
| 80 | + | |
| 81 | + output := runInstaller(t, "--prefix", prefix) | |
| 82 | + | |
| 83 | + if !strings.Contains(output, "not on your PATH") { | |
| 84 | + t.Errorf("the installer did not warn about the PATH:\n%s", output) | |
| 85 | + } | |
| 86 | + if !strings.Contains(output, "export PATH=") { | |
| 87 | + t.Errorf("the installer warned without saying how to fix it:\n%s", output) | |
| 88 | + } | |
| 89 | +} | |
| 90 | + | |
| 91 | +func TestTheInstallerRemovesWhatItInstalled(t *testing.T) { | |
| 92 | + skipUnlessShellIsAvailable(t) | |
| 93 | + prefix := t.TempDir() | |
| 94 | + runInstaller(t, "--prefix", prefix) | |
| 95 | + | |
| 96 | + runInstaller(t, "--prefix", prefix, "--uninstall") | |
| 97 | + | |
| 98 | + if _, err := os.Stat(filepath.Join(prefix, "turbo-golo")); !os.IsNotExist(err) { | |
| 99 | + t.Error("the binary is still there after --uninstall") | |
| 100 | + } | |
| 101 | +} | |
| 102 | + | |
| 103 | +func TestUninstallingNothingIsNotAFailure(t *testing.T) { | |
| 104 | + skipUnlessShellIsAvailable(t) | |
| 105 | + | |
| 106 | + output := runInstaller(t, "--prefix", t.TempDir(), "--uninstall") | |
| 107 | + | |
| 108 | + if !strings.Contains(output, "nothing installed") { | |
| 109 | + t.Errorf("the installer did not say there was nothing to remove:\n%s", output) | |
| 110 | + } | |
| 111 | +} | |
| 112 | + | |
| 113 | +func TestTheInstallerExplainsItself(t *testing.T) { | |
| 114 | + skipUnlessShellIsAvailable(t) | |
| 115 | + | |
| 116 | + output := runInstaller(t, "--help") | |
| 117 | + | |
| 118 | + for _, want := range []string{"--prefix", "--with-server", "--uninstall"} { | |
| 119 | + if !strings.Contains(output, want) { | |
| 120 | + t.Errorf("--help does not document %q:\n%s", want, output) | |
| 121 | + } | |
| 122 | + } | |
| 123 | +} | |
| 124 | + | |
| 125 | +func TestTheInstallerRefusesAnUnknownOption(t *testing.T) { | |
| 126 | + skipUnlessShellIsAvailable(t) | |
| 127 | + | |
| 128 | + output, err := exec.Command("bash", "scripts/install.sh", "--nonsense").CombinedOutput() | |
| 129 | + | |
| 130 | + if err == nil { | |
| 131 | + t.Fatal("the installer accepted an option it does not have") | |
| 132 | + } | |
| 133 | + if !strings.Contains(string(output), "unknown option") { | |
| 134 | + t.Errorf("the installer did not say what was wrong:\n%s", output) | |
| 135 | + } | |
| 136 | +} | |
| 137 | + | |
| 138 | +func TestTheInstallerNeedsADirectoryAfterPrefix(t *testing.T) { | |
| 139 | + skipUnlessShellIsAvailable(t) | |
| 140 | + | |
| 141 | + output, err := exec.Command("bash", "scripts/install.sh", "--prefix").CombinedOutput() | |
| 142 | + | |
| 143 | + if err == nil { | |
| 144 | + t.Fatal("--prefix was accepted with nothing after it") | |
| 145 | + } | |
| 146 | + if !strings.Contains(string(output), "needs a directory") { | |
| 147 | + t.Errorf("the installer did not say what was wrong:\n%s", output) | |
| 148 | + } | |
| 149 | +} | |
| 150 | + | |
| 151 | +func TestTheInstallerRunsFromAnyDirectory(t *testing.T) { | |
| 152 | + skipUnlessShellIsAvailable(t) | |
| 153 | + repo, err := filepath.Abs(".") | |
| 154 | + if err != nil { | |
| 155 | + t.Fatalf("Abs() error = %v", err) | |
| 156 | + } | |
| 157 | + prefix := t.TempDir() | |
| 158 | + | |
| 159 | + // It is invoked by absolute path from somewhere else entirely, as it would | |
| 160 | + // be from a shell alias or another script. | |
| 161 | + command := exec.Command("bash", filepath.Join(repo, "scripts", "install.sh"), "--prefix", prefix) | |
| 162 | + command.Dir = t.TempDir() | |
| 163 | + if output, err := command.CombinedOutput(); err != nil { | |
| 164 | + t.Fatalf("the installer failed when run from elsewhere: %v\n%s", err, output) | |
| 165 | + } | |
| 166 | + | |
| 167 | + if _, err := os.Stat(filepath.Join(prefix, "turbo-golo")); err != nil { | |
| 168 | + t.Errorf("nothing was installed: %v", err) | |
| 169 | + } | |
| 170 | +} | |
| 171 | + | |
| 172 | +func TestAFailedBuildLeavesTheInstalledBinaryAlone(t *testing.T) { | |
| 173 | + skipUnlessShellIsAvailable(t) | |
| 174 | + prefix := t.TempDir() | |
| 175 | + runInstaller(t, "--prefix", prefix) | |
| 176 | + | |
| 177 | + binary := filepath.Join(prefix, "turbo-golo") | |
| 178 | + before, err := os.Stat(binary) | |
| 179 | + if err != nil { | |
| 180 | + t.Fatalf("the first install produced nothing: %v", err) | |
| 181 | + } | |
| 182 | + | |
| 183 | + // A stray file in package main is exactly what a user's own scratch file | |
| 184 | + // does to this repository, and it must not cost them their installation. | |
| 185 | + stray := filepath.Join("scripts", "..", "zz_broken_on_purpose.go") | |
| 186 | + if err := os.WriteFile(stray, []byte("package main\n\nfunc main() {}\n"), 0o644); err != nil { | |
| 187 | + t.Fatalf("writing the stray file: %v", err) | |
| 188 | + } | |
| 189 | + t.Cleanup(func() { os.Remove(stray) }) | |
| 190 | + | |
| 191 | + output, err := exec.Command("bash", "scripts/install.sh", "--prefix", prefix).CombinedOutput() | |
| 192 | + | |
| 193 | + if err == nil { | |
| 194 | + t.Fatal("the installer reported success on a build that cannot succeed") | |
| 195 | + } | |
| 196 | + if !strings.Contains(string(output), "nothing was installed") { | |
| 197 | + t.Errorf("the installer did not say the installation was untouched:\n%s", output) | |
| 198 | + } | |
| 199 | + after, err := os.Stat(binary) | |
| 200 | + if err != nil { | |
| 201 | + t.Fatalf("the failed build removed the installed binary: %v", err) | |
| 202 | + } | |
| 203 | + if !after.ModTime().Equal(before.ModTime()) { | |
| 204 | + t.Error("the failed build replaced the installed binary") | |
| 205 | + } | |
| 206 | +} | |
| 207 | + | |
| 208 | +func TestReinstallingReplacesTheFileRatherThanOverwritingIt(t *testing.T) { | |
| 209 | + // macOS caches a binary's code signature against its inode. Writing new | |
| 210 | + // bytes into the same inode — which is what cp does — leaves the cached | |
| 211 | + // signature describing something else, and the kernel then refuses to | |
| 212 | + // execute it: builds fine, installs fine, "does not run". Replacing the | |
| 213 | + // directory entry with a fresh inode is what avoids that, and it makes the | |
| 214 | + // install atomic besides. | |
| 215 | + skipUnlessShellIsAvailable(t) | |
| 216 | + prefix := t.TempDir() | |
| 217 | + binary := filepath.Join(prefix, "turbo-golo") | |
| 218 | + | |
| 219 | + runInstaller(t, "--prefix", prefix) | |
| 220 | + first := inodeOf(t, binary) | |
| 221 | + | |
| 222 | + runInstaller(t, "--prefix", prefix) | |
| 223 | + second := inodeOf(t, binary) | |
| 224 | + | |
| 225 | + if first == second { | |
| 226 | + t.Errorf("the reinstall wrote into the same inode (%d); it must replace the file", first) | |
| 227 | + } | |
| 228 | +} | |
| 229 | + | |
| 230 | +func TestReinstallingLeavesAWorkingBinary(t *testing.T) { | |
| 231 | + skipUnlessShellIsAvailable(t) | |
| 232 | + prefix := t.TempDir() | |
| 233 | + binary := filepath.Join(prefix, "turbo-golo") | |
| 234 | + | |
| 235 | + runInstaller(t, "--prefix", prefix) | |
| 236 | + runInstaller(t, "--prefix", prefix) | |
| 237 | + | |
| 238 | + if _, err := exec.Command(binary, "-version").Output(); err != nil { | |
| 239 | + t.Fatalf("the reinstalled binary does not run: %v", err) | |
| 240 | + } | |
| 241 | +} | |
| 242 | + | |
| 243 | +func TestABinaryThatWillNotRunIsReportedWithItsOwnError(t *testing.T) { | |
| 244 | + // "the installed binary does not run" on its own tells whoever hit it | |
| 245 | + // nothing they can act on. Whatever the system said has to come through. | |
| 246 | + skipUnlessShellIsAvailable(t) | |
| 247 | + | |
| 248 | + if !strings.Contains(readInstaller(t), "$verify") { | |
| 249 | + t.Error("the installer discards what the binary said when it will not run") | |
| 250 | + } | |
| 251 | +} | |
| 252 | + | |
| 253 | +// readInstaller returns the installer's source. | |
| 254 | +func readInstaller(t *testing.T) string { | |
| 255 | + t.Helper() | |
| 256 | + | |
| 257 | + data, err := os.ReadFile("scripts/install.sh") | |
| 258 | + if err != nil { | |
| 259 | + t.Fatalf("reading the installer: %v", err) | |
| 260 | + } | |
| 261 | + return string(data) | |
| 262 | +} | |
| 263 | + | |
| 264 | +// inodeOf returns a file's inode number. | |
| 265 | +func inodeOf(t *testing.T, path string) uint64 { | |
| 266 | + t.Helper() | |
| 267 | + | |
| 268 | + info, err := os.Stat(path) | |
| 269 | + if err != nil { | |
| 270 | + t.Fatalf("stat %s: %v", path, err) | |
| 271 | + } | |
| 272 | + stat, ok := info.Sys().(*syscall.Stat_t) | |
| 273 | + if !ok { | |
| 274 | + t.Skip("inode numbers are not available on this platform") | |
| 275 | + } | |
| 276 | + return uint64(stat.Ino) | |
| 277 | +} | |
| 278 | + | |
| 279 | +func TestTheInstalledBinaryReportsTheCommitItWasBuiltFrom(t *testing.T) { | |
| 280 | + // The point of stamping: an installed editor must name the commit it came | |
| 281 | + // from, not a constant somebody forgot to bump before releasing. | |
| 282 | + skipUnlessShellIsAvailable(t) | |
| 283 | + prefix := t.TempDir() | |
| 284 | + | |
| 285 | + runInstaller(t, "--prefix", prefix) | |
| 286 | + | |
| 287 | + reported, err := exec.Command(filepath.Join(prefix, "turbo-golo"), "-version").Output() | |
| 288 | + if err != nil { | |
| 289 | + t.Fatalf("the installed binary does not run: %v", err) | |
| 290 | + } | |
| 291 | + | |
| 292 | + commit, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output() | |
| 293 | + if err != nil { | |
| 294 | + t.Skip("not a git checkout, so there is no commit to stamp") | |
| 295 | + } | |
| 296 | + if want := strings.TrimSpace(string(commit)); !strings.Contains(string(reported), want) { | |
| 297 | + t.Errorf("-version printed %q, which never mentions the commit %s", reported, want) | |
| 298 | + } | |
| 299 | +} | |
| 300 | + | |
| 301 | +func TestTheInstalledBinaryDoesNotReportAnUnknownVersion(t *testing.T) { | |
| 302 | + // "unknown" is what the binary says when *no* source could name it, and | |
| 303 | + // seeing it here would mean the installer's ldflags never reached the | |
| 304 | + // linker. "devel" is a different thing: it is what a correct build of a | |
| 305 | + // checkout with no tags reports, so a checkout that has never been tagged | |
| 306 | + // must not fail this. | |
| 307 | + // | |
| 308 | + // What proves the stamp arrived either way is the commit, which only the | |
| 309 | + // linker can have supplied. | |
| 310 | + skipUnlessShellIsAvailable(t) | |
| 311 | + | |
| 312 | + // Outside a git checkout the installer has nothing to stamp *with*, and | |
| 313 | + // "unknown" is then the correct answer rather than a failure — so the | |
| 314 | + // premise is checked before anything is asserted on. | |
| 315 | + commit, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output() | |
| 316 | + if err != nil { | |
| 317 | + t.Skip("not a git checkout, so there is nothing for the installer to stamp") | |
| 318 | + } | |
| 319 | + prefix := t.TempDir() | |
| 320 | + | |
| 321 | + runInstaller(t, "--prefix", prefix) | |
| 322 | + | |
| 323 | + reported, err := exec.Command(filepath.Join(prefix, "turbo-golo"), "-version").Output() | |
| 324 | + if err != nil { | |
| 325 | + t.Fatalf("the installed binary does not run: %v", err) | |
| 326 | + } | |
| 327 | + if strings.Contains(string(reported), "unknown") { | |
| 328 | + t.Errorf("-version printed %q, so nothing reached the linker at all", reported) | |
| 329 | + } | |
| 330 | + if want := strings.TrimSpace(string(commit)); !strings.Contains(string(reported), want) { | |
| 331 | + t.Errorf("-version printed %q, want it to carry the commit %q", reported, want) | |
| 332 | + } | |
| 333 | +} | |
| 334 | + | |
| 335 | +func TestTheInstallerStampsThroughTheLinker(t *testing.T) { | |
| 336 | + // A build outside a git checkout has nothing to describe, and must still | |
| 337 | + // build rather than passing a half-built -X flag to the linker. | |
| 338 | + script := readInstaller(t) | |
| 339 | + | |
| 340 | + for _, want := range []string{"turbo-core/version", "-ldflags", "describe --tags --dirty"} { | |
| 341 | + if !strings.Contains(script, want) { | |
| 342 | + t.Errorf("the installer never mentions %q", want) | |
| 343 | + } | |
| 344 | + } | |
| 345 | + if !strings.Contains(script, `ldflags=""`) { | |
| 346 | + t.Error("the installer has no path for a checkout git cannot describe") | |
| 347 | + } | |
| 348 | +} | |
| 349 | + | |
| 350 | +// The server is the interpreter in language-server mode, so there is nothing | |
| 351 | +// to install beside it — but there is something to say: a user who has golo | |
| 352 | +// has completion already, and the installer is where they find that out. | |
| 353 | +func TestTheInstallerSaysTheInterpreterIsTheServer(t *testing.T) { | |
| 354 | + script := readInstaller(t) | |
| 355 | + | |
| 356 | + for _, want := range []string{"golo lsp", "readonly SERVER=golo"} { | |
| 357 | + if !strings.Contains(script, want) { | |
| 358 | + t.Errorf("the installer never mentions %q", want) | |
| 359 | + } | |
| 360 | + } | |
| 361 | +} | |
| 362 | + | |
| 363 | +// The install hint on the status bar and the command the installer runs must be | |
| 364 | +// the same one. Two spellings of "how do I get this" is how one of them goes | |
| 365 | +// stale without anybody noticing. | |
| 366 | +func TestTheInstallerRunsTheCommandTheEditorRecommends(t *testing.T) { | |
| 367 | + script := readInstaller(t) | |
| 368 | + | |
| 369 | + if !strings.Contains(script, "codeberg.org/TypeUnsafe/golo-script") { | |
| 370 | + t.Error("the installer does not name the GoloScript repository the editor's hint names") | |
| 371 | + } | |
| 372 | +} | |
| 373 | + | |
| 374 | +// Finding the file is not the same as its running, and this family has been | |
| 375 | +// caught by that twice — rustup's shim for Turbo Rust, and a stale tool | |
| 376 | +// directory in Turbo Python. | |
| 377 | +func TestTheInstallerRunsTheServerRatherThanStattingIt(t *testing.T) { | |
| 378 | + if !strings.Contains(readInstaller(t), `"$candidate" --version`) { | |
| 379 | + t.Error("find_server never runs the candidate; an unusable shim would be reported as installed") | |
| 380 | + } | |
| 381 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,381 @@ | |||
| 1 | +package main | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "os" | ||
| 5 | + "os/exec" | ||
| 6 | + "path/filepath" | ||
| 7 | + "runtime" | ||
| 8 | + "strings" | ||
| 9 | + "syscall" | ||
| 10 | + "testing" | ||
| 11 | +) | ||
| 12 | + | ||
| 13 | +// runInstaller runs scripts/install.sh with the given arguments and returns | ||
| 14 | +// everything it printed, failing the test if it did not exit cleanly. | ||
| 15 | +func runInstaller(t *testing.T, args ...string) string { | ||
| 16 | + t.Helper() | ||
| 17 | + | ||
| 18 | + output, err := exec.Command("bash", append([]string{"scripts/install.sh"}, args...)...).CombinedOutput() | ||
| 19 | + if err != nil { | ||
| 20 | + t.Fatalf("scripts/install.sh %v failed: %v\n%s", args, err, output) | ||
| 21 | + } | ||
| 22 | + return string(output) | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +// skipUnlessShellIsAvailable skips a test where the installer cannot run. | ||
| 26 | +func skipUnlessShellIsAvailable(t *testing.T) { | ||
| 27 | + t.Helper() | ||
| 28 | + | ||
| 29 | + if testing.Short() { | ||
| 30 | + t.Skip("-short: the installer compiles the whole editor") | ||
| 31 | + } | ||
| 32 | + if runtime.GOOS == "windows" { | ||
| 33 | + t.Skip("the installer is a shell script") | ||
| 34 | + } | ||
| 35 | + if _, err := exec.LookPath("bash"); err != nil { | ||
| 36 | + t.Skip("bash is not available") | ||
| 37 | + } | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +func TestTheInstallerBuildsAWorkingBinary(t *testing.T) { | ||
| 41 | + skipUnlessShellIsAvailable(t) | ||
| 42 | + prefix := t.TempDir() | ||
| 43 | + | ||
| 44 | + output := runInstaller(t, "--prefix", prefix) | ||
| 45 | + | ||
| 46 | + binary := filepath.Join(prefix, "turbo-golo") | ||
| 47 | + info, err := os.Stat(binary) | ||
| 48 | + if err != nil { | ||
| 49 | + t.Fatalf("nothing was installed at %s: %v\n%s", binary, err, output) | ||
| 50 | + } | ||
| 51 | + if info.Mode().Perm()&0o111 == 0 { | ||
| 52 | + t.Errorf("the installed file has permissions %o, want it executable", info.Mode().Perm()) | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + version, err := exec.Command(binary, "-version").Output() | ||
| 56 | + if err != nil { | ||
| 57 | + t.Fatalf("the installed binary does not run: %v", err) | ||
| 58 | + } | ||
| 59 | + if !strings.Contains(string(version), "Turbo Golo") { | ||
| 60 | + t.Errorf("-version printed %q", version) | ||
| 61 | + } | ||
| 62 | +} | ||
| 63 | + | ||
| 64 | +func TestTheInstallerSaysWhereItPutThings(t *testing.T) { | ||
| 65 | + skipUnlessShellIsAvailable(t) | ||
| 66 | + prefix := t.TempDir() | ||
| 67 | + | ||
| 68 | + output := runInstaller(t, "--prefix", prefix) | ||
| 69 | + | ||
| 70 | + for _, want := range []string{"Turbo Golo", prefix, "PATH", "golo"} { | ||
| 71 | + if !strings.Contains(output, want) { | ||
| 72 | + t.Errorf("the installer never mentions %q:\n%s", want, output) | ||
| 73 | + } | ||
| 74 | + } | ||
| 75 | +} | ||
| 76 | + | ||
| 77 | +func TestTheInstallerWarnsWhenThePrefixIsNotOnPath(t *testing.T) { | ||
| 78 | + skipUnlessShellIsAvailable(t) | ||
| 79 | + prefix := t.TempDir() // a fresh temporary directory is never on PATH | ||
| 80 | + | ||
| 81 | + output := runInstaller(t, "--prefix", prefix) | ||
| 82 | + | ||
| 83 | + if !strings.Contains(output, "not on your PATH") { | ||
| 84 | + t.Errorf("the installer did not warn about the PATH:\n%s", output) | ||
| 85 | + } | ||
| 86 | + if !strings.Contains(output, "export PATH=") { | ||
| 87 | + t.Errorf("the installer warned without saying how to fix it:\n%s", output) | ||
| 88 | + } | ||
| 89 | +} | ||
| 90 | + | ||
| 91 | +func TestTheInstallerRemovesWhatItInstalled(t *testing.T) { | ||
| 92 | + skipUnlessShellIsAvailable(t) | ||
| 93 | + prefix := t.TempDir() | ||
| 94 | + runInstaller(t, "--prefix", prefix) | ||
| 95 | + | ||
| 96 | + runInstaller(t, "--prefix", prefix, "--uninstall") | ||
| 97 | + | ||
| 98 | + if _, err := os.Stat(filepath.Join(prefix, "turbo-golo")); !os.IsNotExist(err) { | ||
| 99 | + t.Error("the binary is still there after --uninstall") | ||
| 100 | + } | ||
| 101 | +} | ||
| 102 | + | ||
| 103 | +func TestUninstallingNothingIsNotAFailure(t *testing.T) { | ||
| 104 | + skipUnlessShellIsAvailable(t) | ||
| 105 | + | ||
| 106 | + output := runInstaller(t, "--prefix", t.TempDir(), "--uninstall") | ||
| 107 | + | ||
| 108 | + if !strings.Contains(output, "nothing installed") { | ||
| 109 | + t.Errorf("the installer did not say there was nothing to remove:\n%s", output) | ||
| 110 | + } | ||
| 111 | +} | ||
| 112 | + | ||
| 113 | +func TestTheInstallerExplainsItself(t *testing.T) { | ||
| 114 | + skipUnlessShellIsAvailable(t) | ||
| 115 | + | ||
| 116 | + output := runInstaller(t, "--help") | ||
| 117 | + | ||
| 118 | + for _, want := range []string{"--prefix", "--with-server", "--uninstall"} { | ||
| 119 | + if !strings.Contains(output, want) { | ||
| 120 | + t.Errorf("--help does not document %q:\n%s", want, output) | ||
| 121 | + } | ||
| 122 | + } | ||
| 123 | +} | ||
| 124 | + | ||
| 125 | +func TestTheInstallerRefusesAnUnknownOption(t *testing.T) { | ||
| 126 | + skipUnlessShellIsAvailable(t) | ||
| 127 | + | ||
| 128 | + output, err := exec.Command("bash", "scripts/install.sh", "--nonsense").CombinedOutput() | ||
| 129 | + | ||
| 130 | + if err == nil { | ||
| 131 | + t.Fatal("the installer accepted an option it does not have") | ||
| 132 | + } | ||
| 133 | + if !strings.Contains(string(output), "unknown option") { | ||
| 134 | + t.Errorf("the installer did not say what was wrong:\n%s", output) | ||
| 135 | + } | ||
| 136 | +} | ||
| 137 | + | ||
| 138 | +func TestTheInstallerNeedsADirectoryAfterPrefix(t *testing.T) { | ||
| 139 | + skipUnlessShellIsAvailable(t) | ||
| 140 | + | ||
| 141 | + output, err := exec.Command("bash", "scripts/install.sh", "--prefix").CombinedOutput() | ||
| 142 | + | ||
| 143 | + if err == nil { | ||
| 144 | + t.Fatal("--prefix was accepted with nothing after it") | ||
| 145 | + } | ||
| 146 | + if !strings.Contains(string(output), "needs a directory") { | ||
| 147 | + t.Errorf("the installer did not say what was wrong:\n%s", output) | ||
| 148 | + } | ||
| 149 | +} | ||
| 150 | + | ||
| 151 | +func TestTheInstallerRunsFromAnyDirectory(t *testing.T) { | ||
| 152 | + skipUnlessShellIsAvailable(t) | ||
| 153 | + repo, err := filepath.Abs(".") | ||
| 154 | + if err != nil { | ||
| 155 | + t.Fatalf("Abs() error = %v", err) | ||
| 156 | + } | ||
| 157 | + prefix := t.TempDir() | ||
| 158 | + | ||
| 159 | + // It is invoked by absolute path from somewhere else entirely, as it would | ||
| 160 | + // be from a shell alias or another script. | ||
| 161 | + command := exec.Command("bash", filepath.Join(repo, "scripts", "install.sh"), "--prefix", prefix) | ||
| 162 | + command.Dir = t.TempDir() | ||
| 163 | + if output, err := command.CombinedOutput(); err != nil { | ||
| 164 | + t.Fatalf("the installer failed when run from elsewhere: %v\n%s", err, output) | ||
| 165 | + } | ||
| 166 | + | ||
| 167 | + if _, err := os.Stat(filepath.Join(prefix, "turbo-golo")); err != nil { | ||
| 168 | + t.Errorf("nothing was installed: %v", err) | ||
| 169 | + } | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +func TestAFailedBuildLeavesTheInstalledBinaryAlone(t *testing.T) { | ||
| 173 | + skipUnlessShellIsAvailable(t) | ||
| 174 | + prefix := t.TempDir() | ||
| 175 | + runInstaller(t, "--prefix", prefix) | ||
| 176 | + | ||
| 177 | + binary := filepath.Join(prefix, "turbo-golo") | ||
| 178 | + before, err := os.Stat(binary) | ||
| 179 | + if err != nil { | ||
| 180 | + t.Fatalf("the first install produced nothing: %v", err) | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + // A stray file in package main is exactly what a user's own scratch file | ||
| 184 | + // does to this repository, and it must not cost them their installation. | ||
| 185 | + stray := filepath.Join("scripts", "..", "zz_broken_on_purpose.go") | ||
| 186 | + if err := os.WriteFile(stray, []byte("package main\n\nfunc main() {}\n"), 0o644); err != nil { | ||
| 187 | + t.Fatalf("writing the stray file: %v", err) | ||
| 188 | + } | ||
| 189 | + t.Cleanup(func() { os.Remove(stray) }) | ||
| 190 | + | ||
| 191 | + output, err := exec.Command("bash", "scripts/install.sh", "--prefix", prefix).CombinedOutput() | ||
| 192 | + | ||
| 193 | + if err == nil { | ||
| 194 | + t.Fatal("the installer reported success on a build that cannot succeed") | ||
| 195 | + } | ||
| 196 | + if !strings.Contains(string(output), "nothing was installed") { | ||
| 197 | + t.Errorf("the installer did not say the installation was untouched:\n%s", output) | ||
| 198 | + } | ||
| 199 | + after, err := os.Stat(binary) | ||
| 200 | + if err != nil { | ||
| 201 | + t.Fatalf("the failed build removed the installed binary: %v", err) | ||
| 202 | + } | ||
| 203 | + if !after.ModTime().Equal(before.ModTime()) { | ||
| 204 | + t.Error("the failed build replaced the installed binary") | ||
| 205 | + } | ||
| 206 | +} | ||
| 207 | + | ||
| 208 | +func TestReinstallingReplacesTheFileRatherThanOverwritingIt(t *testing.T) { | ||
| 209 | + // macOS caches a binary's code signature against its inode. Writing new | ||
| 210 | + // bytes into the same inode — which is what cp does — leaves the cached | ||
| 211 | + // signature describing something else, and the kernel then refuses to | ||
| 212 | + // execute it: builds fine, installs fine, "does not run". Replacing the | ||
| 213 | + // directory entry with a fresh inode is what avoids that, and it makes the | ||
| 214 | + // install atomic besides. | ||
| 215 | + skipUnlessShellIsAvailable(t) | ||
| 216 | + prefix := t.TempDir() | ||
| 217 | + binary := filepath.Join(prefix, "turbo-golo") | ||
| 218 | + | ||
| 219 | + runInstaller(t, "--prefix", prefix) | ||
| 220 | + first := inodeOf(t, binary) | ||
| 221 | + | ||
| 222 | + runInstaller(t, "--prefix", prefix) | ||
| 223 | + second := inodeOf(t, binary) | ||
| 224 | + | ||
| 225 | + if first == second { | ||
| 226 | + t.Errorf("the reinstall wrote into the same inode (%d); it must replace the file", first) | ||
| 227 | + } | ||
| 228 | +} | ||
| 229 | + | ||
| 230 | +func TestReinstallingLeavesAWorkingBinary(t *testing.T) { | ||
| 231 | + skipUnlessShellIsAvailable(t) | ||
| 232 | + prefix := t.TempDir() | ||
| 233 | + binary := filepath.Join(prefix, "turbo-golo") | ||
| 234 | + | ||
| 235 | + runInstaller(t, "--prefix", prefix) | ||
| 236 | + runInstaller(t, "--prefix", prefix) | ||
| 237 | + | ||
| 238 | + if _, err := exec.Command(binary, "-version").Output(); err != nil { | ||
| 239 | + t.Fatalf("the reinstalled binary does not run: %v", err) | ||
| 240 | + } | ||
| 241 | +} | ||
| 242 | + | ||
| 243 | +func TestABinaryThatWillNotRunIsReportedWithItsOwnError(t *testing.T) { | ||
| 244 | + // "the installed binary does not run" on its own tells whoever hit it | ||
| 245 | + // nothing they can act on. Whatever the system said has to come through. | ||
| 246 | + skipUnlessShellIsAvailable(t) | ||
| 247 | + | ||
| 248 | + if !strings.Contains(readInstaller(t), "$verify") { | ||
| 249 | + t.Error("the installer discards what the binary said when it will not run") | ||
| 250 | + } | ||
| 251 | +} | ||
| 252 | + | ||
| 253 | +// readInstaller returns the installer's source. | ||
| 254 | +func readInstaller(t *testing.T) string { | ||
| 255 | + t.Helper() | ||
| 256 | + | ||
| 257 | + data, err := os.ReadFile("scripts/install.sh") | ||
| 258 | + if err != nil { | ||
| 259 | + t.Fatalf("reading the installer: %v", err) | ||
| 260 | + } | ||
| 261 | + return string(data) | ||
| 262 | +} | ||
| 263 | + | ||
| 264 | +// inodeOf returns a file's inode number. | ||
| 265 | +func inodeOf(t *testing.T, path string) uint64 { | ||
| 266 | + t.Helper() | ||
| 267 | + | ||
| 268 | + info, err := os.Stat(path) | ||
| 269 | + if err != nil { | ||
| 270 | + t.Fatalf("stat %s: %v", path, err) | ||
| 271 | + } | ||
| 272 | + stat, ok := info.Sys().(*syscall.Stat_t) | ||
| 273 | + if !ok { | ||
| 274 | + t.Skip("inode numbers are not available on this platform") | ||
| 275 | + } | ||
| 276 | + return uint64(stat.Ino) | ||
| 277 | +} | ||
| 278 | + | ||
| 279 | +func TestTheInstalledBinaryReportsTheCommitItWasBuiltFrom(t *testing.T) { | ||
| 280 | + // The point of stamping: an installed editor must name the commit it came | ||
| 281 | + // from, not a constant somebody forgot to bump before releasing. | ||
| 282 | + skipUnlessShellIsAvailable(t) | ||
| 283 | + prefix := t.TempDir() | ||
| 284 | + | ||
| 285 | + runInstaller(t, "--prefix", prefix) | ||
| 286 | + | ||
| 287 | + reported, err := exec.Command(filepath.Join(prefix, "turbo-golo"), "-version").Output() | ||
| 288 | + if err != nil { | ||
| 289 | + t.Fatalf("the installed binary does not run: %v", err) | ||
| 290 | + } | ||
| 291 | + | ||
| 292 | + commit, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output() | ||
| 293 | + if err != nil { | ||
| 294 | + t.Skip("not a git checkout, so there is no commit to stamp") | ||
| 295 | + } | ||
| 296 | + if want := strings.TrimSpace(string(commit)); !strings.Contains(string(reported), want) { | ||
| 297 | + t.Errorf("-version printed %q, which never mentions the commit %s", reported, want) | ||
| 298 | + } | ||
| 299 | +} | ||
| 300 | + | ||
| 301 | +func TestTheInstalledBinaryDoesNotReportAnUnknownVersion(t *testing.T) { | ||
| 302 | + // "unknown" is what the binary says when *no* source could name it, and | ||
| 303 | + // seeing it here would mean the installer's ldflags never reached the | ||
| 304 | + // linker. "devel" is a different thing: it is what a correct build of a | ||
| 305 | + // checkout with no tags reports, so a checkout that has never been tagged | ||
| 306 | + // must not fail this. | ||
| 307 | + // | ||
| 308 | + // What proves the stamp arrived either way is the commit, which only the | ||
| 309 | + // linker can have supplied. | ||
| 310 | + skipUnlessShellIsAvailable(t) | ||
| 311 | + | ||
| 312 | + // Outside a git checkout the installer has nothing to stamp *with*, and | ||
| 313 | + // "unknown" is then the correct answer rather than a failure — so the | ||
| 314 | + // premise is checked before anything is asserted on. | ||
| 315 | + commit, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output() | ||
| 316 | + if err != nil { | ||
| 317 | + t.Skip("not a git checkout, so there is nothing for the installer to stamp") | ||
| 318 | + } | ||
| 319 | + prefix := t.TempDir() | ||
| 320 | + | ||
| 321 | + runInstaller(t, "--prefix", prefix) | ||
| 322 | + | ||
| 323 | + reported, err := exec.Command(filepath.Join(prefix, "turbo-golo"), "-version").Output() | ||
| 324 | + if err != nil { | ||
| 325 | + t.Fatalf("the installed binary does not run: %v", err) | ||
| 326 | + } | ||
| 327 | + if strings.Contains(string(reported), "unknown") { | ||
| 328 | + t.Errorf("-version printed %q, so nothing reached the linker at all", reported) | ||
| 329 | + } | ||
| 330 | + if want := strings.TrimSpace(string(commit)); !strings.Contains(string(reported), want) { | ||
| 331 | + t.Errorf("-version printed %q, want it to carry the commit %q", reported, want) | ||
| 332 | + } | ||
| 333 | +} | ||
| 334 | + | ||
| 335 | +func TestTheInstallerStampsThroughTheLinker(t *testing.T) { | ||
| 336 | + // A build outside a git checkout has nothing to describe, and must still | ||
| 337 | + // build rather than passing a half-built -X flag to the linker. | ||
| 338 | + script := readInstaller(t) | ||
| 339 | + | ||
| 340 | + for _, want := range []string{"turbo-core/version", "-ldflags", "describe --tags --dirty"} { | ||
| 341 | + if !strings.Contains(script, want) { | ||
| 342 | + t.Errorf("the installer never mentions %q", want) | ||
| 343 | + } | ||
| 344 | + } | ||
| 345 | + if !strings.Contains(script, `ldflags=""`) { | ||
| 346 | + t.Error("the installer has no path for a checkout git cannot describe") | ||
| 347 | + } | ||
| 348 | +} | ||
| 349 | + | ||
| 350 | +// The server is the interpreter in language-server mode, so there is nothing | ||
| 351 | +// to install beside it — but there is something to say: a user who has golo | ||
| 352 | +// has completion already, and the installer is where they find that out. | ||
| 353 | +func TestTheInstallerSaysTheInterpreterIsTheServer(t *testing.T) { | ||
| 354 | + script := readInstaller(t) | ||
| 355 | + | ||
| 356 | + for _, want := range []string{"golo lsp", "readonly SERVER=golo"} { | ||
| 357 | + if !strings.Contains(script, want) { | ||
| 358 | + t.Errorf("the installer never mentions %q", want) | ||
| 359 | + } | ||
| 360 | + } | ||
| 361 | +} | ||
| 362 | + | ||
| 363 | +// The install hint on the status bar and the command the installer runs must be | ||
| 364 | +// the same one. Two spellings of "how do I get this" is how one of them goes | ||
| 365 | +// stale without anybody noticing. | ||
| 366 | +func TestTheInstallerRunsTheCommandTheEditorRecommends(t *testing.T) { | ||
| 367 | + script := readInstaller(t) | ||
| 368 | + | ||
| 369 | + if !strings.Contains(script, "codeberg.org/TypeUnsafe/golo-script") { | ||
| 370 | + t.Error("the installer does not name the GoloScript repository the editor's hint names") | ||
| 371 | + } | ||
| 372 | +} | ||
| 373 | + | ||
| 374 | +// Finding the file is not the same as its running, and this family has been | ||
| 375 | +// caught by that twice — rustup's shim for Turbo Rust, and a stale tool | ||
| 376 | +// directory in Turbo Python. | ||
| 377 | +func TestTheInstallerRunsTheServerRatherThanStattingIt(t *testing.T) { | ||
| 378 | + if !strings.Contains(readInstaller(t), `"$candidate" --version`) { | ||
| 379 | + t.Error("find_server never runs the candidate; an unusable shim would be reported as installed") | ||
| 380 | + } | ||
| 381 | +} | ||
added
internal/gololang/acp.toml.tmpl +80 -0 | new file mode 100644 | ||
| @@ -0,0 +1,80 @@ | ||
| 1 | +# turbo-golo agents. | |
| 2 | +# | |
| 3 | +# Each [[agent]] becomes one line of the Agent menu (Alt-A). Choosing it starts | |
| 4 | +# that program and opens a window on the conversation; closing the window stops | |
| 5 | +# it again. Open the same agent twice and you get two independent conversations. | |
| 6 | +# | |
| 7 | +# The editor is a client for the Agent Client Protocol — https://agentclientprotocol.com — | |
| 8 | +# so it holds no API key and knows no model. All of that is your agent's own | |
| 9 | +# configuration, in a file this editor does not read. | |
| 10 | +# | |
| 11 | +# name what the menu shows, and what the window is called. Required, and | |
| 12 | +# unique: a project's agent of the same name replaces one of yours. | |
| 13 | +# command the program to run. Required. Looked up on PATH. | |
| 14 | +# args its arguments, passed as given. There is no shell here, so no | |
| 15 | +# quoting, no globs and no && — use command = "sh", args = ["-c", …] | |
| 16 | +# when you really want one. | |
| 17 | +# env extra environment variables. The agent also inherits the ones the | |
| 18 | +# editor was started with, so a credential already exported reaches | |
| 19 | +# it without being written down here. | |
| 20 | +# cwd where to run it, relative to the project. Left out, it is the | |
| 21 | +# project itself. | |
| 22 | +# | |
| 23 | +# A key this file does not define is refused rather than ignored: a misspelt | |
| 24 | +# `comand` would otherwise look exactly like one that had no effect. | |
| 25 | +# | |
| 26 | +# The same file may also live in %[2]s, where it applies to every project you open. | |
| 27 | +# This one is read afterwards and wins where a name appears in both. | |
| 28 | + | |
| 29 | +# Docker's agent runtime, talking to a model server of your choosing. | |
| 30 | +# `docker agent serve acp <file>` speaks the protocol on its standard input and | |
| 31 | +# output, which is exactly what the editor wants. | |
| 32 | +# | |
| 33 | +# The YAML beside this file is the agent's own: which provider, which model, | |
| 34 | +# which tools. Write it yourself, or run `docker agent new` to start one. | |
| 35 | + | |
| 36 | +[[agent]] | |
| 37 | +name = "Local agent" | |
| 38 | +command = "docker" | |
| 39 | +args = ["agent", "serve", "acp", "%[1]s/agent.yaml"] | |
| 40 | +env = { TELEMETRY_ENABLED = "false" } | |
| 41 | + | |
| 42 | +# A second agent is one more block. Two windows side by side — Window ▸ Tile — | |
| 43 | +# is how a fast local model and a slow careful one get compared. | |
| 44 | +# | |
| 45 | +# [[agent]] | |
| 46 | +# name = "Reviewer" | |
| 47 | +# command = "my-agent" | |
| 48 | +# args = ["--acp", "--profile", "review"] | |
| 49 | +# cwd = "." | |
| 50 | + | |
| 51 | +# Once a window is open: | |
| 52 | +# | |
| 53 | +# Enter send what you have typed | |
| 54 | +# Alt-Enter a new line instead of sending | |
| 55 | +# Tab move between the conversation and the box | |
| 56 | +# / at the start of the box, list the agent's own commands | |
| 57 | +# @ list the project's files; the one you pick goes to the agent | |
| 58 | +# PgUp PgDn read back through the conversation | |
| 59 | +# Esc stop the turn in progress | |
| 60 | +# Ctrl-W close the window, and stop the agent with it | |
| 61 | +# | |
| 62 | +# And in the conversation, with Tab pressed: | |
| 63 | +# | |
| 64 | +# ↑ ↓ move the cursor through what was said | |
| 65 | +# Shift-↑ ↓ select whole lines | |
| 66 | +# Ctrl-C copy — the selection, or the block the cursor is on | |
| 67 | +# | |
| 68 | +# What is copied goes to this editor's clipboard *and*, through the terminal, to | |
| 69 | +# the system's — so Shift-Ins pastes it into a file here, and Ctrl-V pastes it | |
| 70 | +# anywhere else. | |
| 71 | +# | |
| 72 | +# Code the agent sends inside a ```golo fence is coloured by the same scanner | |
| 73 | +# this editor colours .golo files with. A fence naming a language it does not | |
| 74 | +# know is left plain rather than guessed at. | |
| 75 | +# | |
| 76 | +# An agent with a shell or a filesystem tool asks before it uses one, and the | |
| 77 | +# box that appears carries the agent's own choices. Nothing runs until you | |
| 78 | +# answer. When it reads a file you have open and have not saved, it is given | |
| 79 | +# what you can see rather than what is on disk; when it writes one, the change | |
| 80 | +# lands in the buffer for you to undo with Ctrl-Z or keep with F2. | |
| new file mode 100644 | |||
| @@ -0,0 +1,80 @@ | |||
| 1 | +# turbo-golo agents. | ||
| 2 | +# | ||
| 3 | +# Each [[agent]] becomes one line of the Agent menu (Alt-A). Choosing it starts | ||
| 4 | +# that program and opens a window on the conversation; closing the window stops | ||
| 5 | +# it again. Open the same agent twice and you get two independent conversations. | ||
| 6 | +# | ||
| 7 | +# The editor is a client for the Agent Client Protocol — https://agentclientprotocol.com — | ||
| 8 | +# so it holds no API key and knows no model. All of that is your agent's own | ||
| 9 | +# configuration, in a file this editor does not read. | ||
| 10 | +# | ||
| 11 | +# name what the menu shows, and what the window is called. Required, and | ||
| 12 | +# unique: a project's agent of the same name replaces one of yours. | ||
| 13 | +# command the program to run. Required. Looked up on PATH. | ||
| 14 | +# args its arguments, passed as given. There is no shell here, so no | ||
| 15 | +# quoting, no globs and no && — use command = "sh", args = ["-c", …] | ||
| 16 | +# when you really want one. | ||
| 17 | +# env extra environment variables. The agent also inherits the ones the | ||
| 18 | +# editor was started with, so a credential already exported reaches | ||
| 19 | +# it without being written down here. | ||
| 20 | +# cwd where to run it, relative to the project. Left out, it is the | ||
| 21 | +# project itself. | ||
| 22 | +# | ||
| 23 | +# A key this file does not define is refused rather than ignored: a misspelt | ||
| 24 | +# `comand` would otherwise look exactly like one that had no effect. | ||
| 25 | +# | ||
| 26 | +# The same file may also live in %[2]s, where it applies to every project you open. | ||
| 27 | +# This one is read afterwards and wins where a name appears in both. | ||
| 28 | + | ||
| 29 | +# Docker's agent runtime, talking to a model server of your choosing. | ||
| 30 | +# `docker agent serve acp <file>` speaks the protocol on its standard input and | ||
| 31 | +# output, which is exactly what the editor wants. | ||
| 32 | +# | ||
| 33 | +# The YAML beside this file is the agent's own: which provider, which model, | ||
| 34 | +# which tools. Write it yourself, or run `docker agent new` to start one. | ||
| 35 | + | ||
| 36 | +[[agent]] | ||
| 37 | +name = "Local agent" | ||
| 38 | +command = "docker" | ||
| 39 | +args = ["agent", "serve", "acp", "%[1]s/agent.yaml"] | ||
| 40 | +env = { TELEMETRY_ENABLED = "false" } | ||
| 41 | + | ||
| 42 | +# A second agent is one more block. Two windows side by side — Window ▸ Tile — | ||
| 43 | +# is how a fast local model and a slow careful one get compared. | ||
| 44 | +# | ||
| 45 | +# [[agent]] | ||
| 46 | +# name = "Reviewer" | ||
| 47 | +# command = "my-agent" | ||
| 48 | +# args = ["--acp", "--profile", "review"] | ||
| 49 | +# cwd = "." | ||
| 50 | + | ||
| 51 | +# Once a window is open: | ||
| 52 | +# | ||
| 53 | +# Enter send what you have typed | ||
| 54 | +# Alt-Enter a new line instead of sending | ||
| 55 | +# Tab move between the conversation and the box | ||
| 56 | +# / at the start of the box, list the agent's own commands | ||
| 57 | +# @ list the project's files; the one you pick goes to the agent | ||
| 58 | +# PgUp PgDn read back through the conversation | ||
| 59 | +# Esc stop the turn in progress | ||
| 60 | +# Ctrl-W close the window, and stop the agent with it | ||
| 61 | +# | ||
| 62 | +# And in the conversation, with Tab pressed: | ||
| 63 | +# | ||
| 64 | +# ↑ ↓ move the cursor through what was said | ||
| 65 | +# Shift-↑ ↓ select whole lines | ||
| 66 | +# Ctrl-C copy — the selection, or the block the cursor is on | ||
| 67 | +# | ||
| 68 | +# What is copied goes to this editor's clipboard *and*, through the terminal, to | ||
| 69 | +# the system's — so Shift-Ins pastes it into a file here, and Ctrl-V pastes it | ||
| 70 | +# anywhere else. | ||
| 71 | +# | ||
| 72 | +# Code the agent sends inside a ```golo fence is coloured by the same scanner | ||
| 73 | +# this editor colours .golo files with. A fence naming a language it does not | ||
| 74 | +# know is left plain rather than guessed at. | ||
| 75 | +# | ||
| 76 | +# An agent with a shell or a filesystem tool asks before it uses one, and the | ||
| 77 | +# box that appears carries the agent's own choices. Nothing runs until you | ||
| 78 | +# answer. When it reads a file you have open and have not saved, it is given | ||
| 79 | +# what you can see rather than what is on disk; when it writes one, the change | ||
| 80 | +# lands in the buffer for you to undo with Ctrl-Z or keep with F2. | ||
added
internal/gololang/acp_test.go +128 -0 | new file mode 100644 | ||
| @@ -0,0 +1,128 @@ | ||
| 1 | +package gololang | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "errors" | |
| 5 | + "os" | |
| 6 | + "strings" | |
| 7 | + "testing" | |
| 8 | + | |
| 9 | + "rickub.com/turbo-editors/turbo-core/acp" | |
| 10 | +) | |
| 11 | + | |
| 12 | +// The agents file Turbo Golo writes. What the *feature* does is turbo-core's to | |
| 13 | +// test; what is checked here is the one part that belongs to this editor — | |
| 14 | +// that the starter file is filled in correctly, loads back as the agent it | |
| 15 | +// describes, and says enough for somebody to point it at their own. | |
| 16 | + | |
| 17 | +// readAgentsFile returns the agents file a project was given. | |
| 18 | +func readAgentsFile(t *testing.T, dir string) string { | |
| 19 | + t.Helper() | |
| 20 | + | |
| 21 | + data, err := os.ReadFile(acp.ProjectPath(Profile(), dir)) | |
| 22 | + if err != nil { | |
| 23 | + t.Fatalf("reading the agents file: %v", err) | |
| 24 | + } | |
| 25 | + return string(data) | |
| 26 | +} | |
| 27 | + | |
| 28 | +// createAgents writes the starter agents file into a fresh project. | |
| 29 | +func createAgents(t *testing.T) string { | |
| 30 | + t.Helper() | |
| 31 | + | |
| 32 | + dir := t.TempDir() | |
| 33 | + if _, err := acp.Create(Profile(), dir); err != nil { | |
| 34 | + t.Fatalf("acp.Create() error = %v", err) | |
| 35 | + } | |
| 36 | + return dir | |
| 37 | +} | |
| 38 | + | |
| 39 | +func TestTheCreatedAgentsFileFillsBothOfItsBlanks(t *testing.T) { | |
| 40 | + // Two different values — the project directory the example agent points | |
| 41 | + // into, and the user's own file that a comment names. Go writes | |
| 42 | + // %!s(MISSING) into the output rather than failing, so a miscounted verb | |
| 43 | + // produces a starter file that is written, opened, and wrong. | |
| 44 | + contents := readAgentsFile(t, createAgents(t)) | |
| 45 | + | |
| 46 | + if strings.Contains(contents, "%!") { | |
| 47 | + t.Errorf("the created file has an unfilled verb in it:\n%s", contents) | |
| 48 | + } | |
| 49 | + if want := Profile().ProjectDir() + "/agent.yaml"; !strings.Contains(contents, want) { | |
| 50 | + t.Errorf("the example agent does not point at %q:\n%s", want, contents) | |
| 51 | + } | |
| 52 | + if want := acp.UserPath(Profile()); want != "" && !strings.Contains(contents, want) { | |
| 53 | + t.Errorf("the created file never names the user's own file %q:\n%s", want, contents) | |
| 54 | + } | |
| 55 | +} | |
| 56 | + | |
| 57 | +func TestTheCreatedAgentsFileLoadsBackAsOneAgent(t *testing.T) { | |
| 58 | + // The file is mostly comments, and a comment carrying an [[agent]] example | |
| 59 | + // that the loader read as real would put an agent nobody configured into | |
| 60 | + // the menu. | |
| 61 | + dir := createAgents(t) | |
| 62 | + | |
| 63 | + list, err := acp.Load(Profile(), dir) | |
| 64 | + if err != nil { | |
| 65 | + t.Fatalf("acp.Load() error = %v", err) | |
| 66 | + } | |
| 67 | + if list.Len() != 1 { | |
| 68 | + t.Fatalf("the created file holds %d agents, want 1: %v", list.Len(), list.Agents()) | |
| 69 | + } | |
| 70 | + | |
| 71 | + agent := list.Agents()[0] | |
| 72 | + if agent.Command != "docker" { | |
| 73 | + t.Errorf("the example agent runs %q, want docker", agent.Command) | |
| 74 | + } | |
| 75 | + if want := "agent serve acp"; !strings.Contains(agent.CommandLine(), want) { | |
| 76 | + t.Errorf("the example command line is %q, want %q in it", agent.CommandLine(), want) | |
| 77 | + } | |
| 78 | +} | |
| 79 | + | |
| 80 | +func TestTheCreatedAgentsFileExplainsItself(t *testing.T) { | |
| 81 | + // The keys, the window's keyboard and how to copy out of it are all | |
| 82 | + // invisible otherwise: this is the only document the editor hands a user. | |
| 83 | + contents := readAgentsFile(t, createAgents(t)) | |
| 84 | + | |
| 85 | + for _, want := range []string{ | |
| 86 | + "[[agent]]", "name", "command", "args", "env", "cwd", | |
| 87 | + "agentclientprotocol.com", | |
| 88 | + "Alt-Enter", "Ctrl-W", "Esc", "Ctrl-C", | |
| 89 | + } { | |
| 90 | + if !strings.Contains(contents, want) { | |
| 91 | + t.Errorf("the created file never mentions %q:\n%s", want, contents) | |
| 92 | + } | |
| 93 | + } | |
| 94 | +} | |
| 95 | + | |
| 96 | +func TestTheCreatedAgentsFileNamesThisEditorsOwnLanguage(t *testing.T) { | |
| 97 | + // The comment about coloured code blocks is the one line of this file that | |
| 98 | + // is about Turbo Golo rather than about the protocol, and a copy left naming | |
| 99 | + // another editor's language would be the obvious way to get it wrong. | |
| 100 | + contents := readAgentsFile(t, createAgents(t)) | |
| 101 | + | |
| 102 | + if want := "```golo"; !strings.Contains(contents, want) { | |
| 103 | + t.Errorf("the created file never mentions a %q fence:\n%s", want, contents) | |
| 104 | + } | |
| 105 | + if want := ".golo files"; !strings.Contains(contents, want) { | |
| 106 | + t.Errorf("the created file never mentions %q:\n%s", want, contents) | |
| 107 | + } | |
| 108 | +} | |
| 109 | + | |
| 110 | +func TestCreatingAgentsTwiceLeavesTheFirstAlone(t *testing.T) { | |
| 111 | + dir := createAgents(t) | |
| 112 | + path := acp.ProjectPath(Profile(), dir) | |
| 113 | + | |
| 114 | + if err := os.WriteFile(path, []byte("# mine\n"), 0o644); err != nil { | |
| 115 | + t.Fatalf("writing over it: %v", err) | |
| 116 | + } | |
| 117 | + if _, err := acp.Create(Profile(), dir); !errors.Is(err, acp.ErrExists) { | |
| 118 | + t.Errorf("acp.Create() error = %v, want ErrExists", err) | |
| 119 | + } | |
| 120 | + | |
| 121 | + data, err := os.ReadFile(path) | |
| 122 | + if err != nil { | |
| 123 | + t.Fatalf("reading it back: %v", err) | |
| 124 | + } | |
| 125 | + if string(data) != "# mine\n" { | |
| 126 | + t.Errorf("the file was overwritten: %q", data) | |
| 127 | + } | |
| 128 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,128 @@ | |||
| 1 | +package gololang | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "errors" | ||
| 5 | + "os" | ||
| 6 | + "strings" | ||
| 7 | + "testing" | ||
| 8 | + | ||
| 9 | + "rickub.com/turbo-editors/turbo-core/acp" | ||
| 10 | +) | ||
| 11 | + | ||
| 12 | +// The agents file Turbo Golo writes. What the *feature* does is turbo-core's to | ||
| 13 | +// test; what is checked here is the one part that belongs to this editor — | ||
| 14 | +// that the starter file is filled in correctly, loads back as the agent it | ||
| 15 | +// describes, and says enough for somebody to point it at their own. | ||
| 16 | + | ||
| 17 | +// readAgentsFile returns the agents file a project was given. | ||
| 18 | +func readAgentsFile(t *testing.T, dir string) string { | ||
| 19 | + t.Helper() | ||
| 20 | + | ||
| 21 | + data, err := os.ReadFile(acp.ProjectPath(Profile(), dir)) | ||
| 22 | + if err != nil { | ||
| 23 | + t.Fatalf("reading the agents file: %v", err) | ||
| 24 | + } | ||
| 25 | + return string(data) | ||
| 26 | +} | ||
| 27 | + | ||
| 28 | +// createAgents writes the starter agents file into a fresh project. | ||
| 29 | +func createAgents(t *testing.T) string { | ||
| 30 | + t.Helper() | ||
| 31 | + | ||
| 32 | + dir := t.TempDir() | ||
| 33 | + if _, err := acp.Create(Profile(), dir); err != nil { | ||
| 34 | + t.Fatalf("acp.Create() error = %v", err) | ||
| 35 | + } | ||
| 36 | + return dir | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +func TestTheCreatedAgentsFileFillsBothOfItsBlanks(t *testing.T) { | ||
| 40 | + // Two different values — the project directory the example agent points | ||
| 41 | + // into, and the user's own file that a comment names. Go writes | ||
| 42 | + // %!s(MISSING) into the output rather than failing, so a miscounted verb | ||
| 43 | + // produces a starter file that is written, opened, and wrong. | ||
| 44 | + contents := readAgentsFile(t, createAgents(t)) | ||
| 45 | + | ||
| 46 | + if strings.Contains(contents, "%!") { | ||
| 47 | + t.Errorf("the created file has an unfilled verb in it:\n%s", contents) | ||
| 48 | + } | ||
| 49 | + if want := Profile().ProjectDir() + "/agent.yaml"; !strings.Contains(contents, want) { | ||
| 50 | + t.Errorf("the example agent does not point at %q:\n%s", want, contents) | ||
| 51 | + } | ||
| 52 | + if want := acp.UserPath(Profile()); want != "" && !strings.Contains(contents, want) { | ||
| 53 | + t.Errorf("the created file never names the user's own file %q:\n%s", want, contents) | ||
| 54 | + } | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +func TestTheCreatedAgentsFileLoadsBackAsOneAgent(t *testing.T) { | ||
| 58 | + // The file is mostly comments, and a comment carrying an [[agent]] example | ||
| 59 | + // that the loader read as real would put an agent nobody configured into | ||
| 60 | + // the menu. | ||
| 61 | + dir := createAgents(t) | ||
| 62 | + | ||
| 63 | + list, err := acp.Load(Profile(), dir) | ||
| 64 | + if err != nil { | ||
| 65 | + t.Fatalf("acp.Load() error = %v", err) | ||
| 66 | + } | ||
| 67 | + if list.Len() != 1 { | ||
| 68 | + t.Fatalf("the created file holds %d agents, want 1: %v", list.Len(), list.Agents()) | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + agent := list.Agents()[0] | ||
| 72 | + if agent.Command != "docker" { | ||
| 73 | + t.Errorf("the example agent runs %q, want docker", agent.Command) | ||
| 74 | + } | ||
| 75 | + if want := "agent serve acp"; !strings.Contains(agent.CommandLine(), want) { | ||
| 76 | + t.Errorf("the example command line is %q, want %q in it", agent.CommandLine(), want) | ||
| 77 | + } | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +func TestTheCreatedAgentsFileExplainsItself(t *testing.T) { | ||
| 81 | + // The keys, the window's keyboard and how to copy out of it are all | ||
| 82 | + // invisible otherwise: this is the only document the editor hands a user. | ||
| 83 | + contents := readAgentsFile(t, createAgents(t)) | ||
| 84 | + | ||
| 85 | + for _, want := range []string{ | ||
| 86 | + "[[agent]]", "name", "command", "args", "env", "cwd", | ||
| 87 | + "agentclientprotocol.com", | ||
| 88 | + "Alt-Enter", "Ctrl-W", "Esc", "Ctrl-C", | ||
| 89 | + } { | ||
| 90 | + if !strings.Contains(contents, want) { | ||
| 91 | + t.Errorf("the created file never mentions %q:\n%s", want, contents) | ||
| 92 | + } | ||
| 93 | + } | ||
| 94 | +} | ||
| 95 | + | ||
| 96 | +func TestTheCreatedAgentsFileNamesThisEditorsOwnLanguage(t *testing.T) { | ||
| 97 | + // The comment about coloured code blocks is the one line of this file that | ||
| 98 | + // is about Turbo Golo rather than about the protocol, and a copy left naming | ||
| 99 | + // another editor's language would be the obvious way to get it wrong. | ||
| 100 | + contents := readAgentsFile(t, createAgents(t)) | ||
| 101 | + | ||
| 102 | + if want := "```golo"; !strings.Contains(contents, want) { | ||
| 103 | + t.Errorf("the created file never mentions a %q fence:\n%s", want, contents) | ||
| 104 | + } | ||
| 105 | + if want := ".golo files"; !strings.Contains(contents, want) { | ||
| 106 | + t.Errorf("the created file never mentions %q:\n%s", want, contents) | ||
| 107 | + } | ||
| 108 | +} | ||
| 109 | + | ||
| 110 | +func TestCreatingAgentsTwiceLeavesTheFirstAlone(t *testing.T) { | ||
| 111 | + dir := createAgents(t) | ||
| 112 | + path := acp.ProjectPath(Profile(), dir) | ||
| 113 | + | ||
| 114 | + if err := os.WriteFile(path, []byte("# mine\n"), 0o644); err != nil { | ||
| 115 | + t.Fatalf("writing over it: %v", err) | ||
| 116 | + } | ||
| 117 | + if _, err := acp.Create(Profile(), dir); !errors.Is(err, acp.ErrExists) { | ||
| 118 | + t.Errorf("acp.Create() error = %v, want ErrExists", err) | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + data, err := os.ReadFile(path) | ||
| 122 | + if err != nil { | ||
| 123 | + t.Fatalf("reading it back: %v", err) | ||
| 124 | + } | ||
| 125 | + if string(data) != "# mine\n" { | ||
| 126 | + t.Errorf("the file was overwritten: %q", data) | ||
| 127 | + } | ||
| 128 | +} | ||
added
internal/gololang/editor_test.go +691 -0 | new file mode 100644 | ||
| @@ -0,0 +1,691 @@ | ||
| 1 | +package gololang_test | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + "errors" | |
| 6 | + "os" | |
| 7 | + "os/exec" | |
| 8 | + "path/filepath" | |
| 9 | + "slices" | |
| 10 | + "strings" | |
| 11 | + "testing" | |
| 12 | + "time" | |
| 13 | + | |
| 14 | + "github.com/gdamore/tcell/v2" | |
| 15 | + | |
| 16 | + "rickub.com/turbo-editors/turbo-core/app" | |
| 17 | + "rickub.com/turbo-editors/turbo-core/buffer" | |
| 18 | + "rickub.com/turbo-editors/turbo-core/lsp" | |
| 19 | + "rickub.com/turbo-editors/turbo-core/syntax" | |
| 20 | + "rickub.com/turbo-editors/turbo-core/ui" | |
| 21 | + | |
| 22 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | |
| 23 | +) | |
| 24 | + | |
| 25 | +// --- the editor, assembled -------------------------------------------------- | |
| 26 | + | |
| 27 | +func TestTheEditorCallsItselfTurboGolo(t *testing.T) { | |
| 28 | + editor := newTestEditor(t) | |
| 29 | + | |
| 30 | + if got := editor.Profile().Name; got != gololang.Name { | |
| 31 | + t.Errorf("Profile().Name = %q, want %q", got, gololang.Name) | |
| 32 | + } | |
| 33 | + if got := editor.Profile().ProjectDir(); got != ".turbo-golo" { | |
| 34 | + t.Errorf("ProjectDir() = %q, want %q", got, ".turbo-golo") | |
| 35 | + } | |
| 36 | +} | |
| 37 | + | |
| 38 | +func TestTheEditorColoursGoloSourceItOpens(t *testing.T) { | |
| 39 | + // The whole path in one test: Register taught the library about Golo, the | |
| 40 | + // profile named the editor, and a .golo file opened through the public | |
| 41 | + // API comes out coloured. | |
| 42 | + root := t.TempDir() | |
| 43 | + path := filepath.Join(root, "main.golo") | |
| 44 | + writeFile(t, path, "module demo.Main\n\nfunction main = |args| {\n println(\"hi\")\n}\n") | |
| 45 | + | |
| 46 | + editor := newTestEditor(t) | |
| 47 | + editor.Open(path) | |
| 48 | + | |
| 49 | + if got := editor.ActiveView().Language(); got != gololang.Language { | |
| 50 | + t.Fatalf("the view colours the file as %q, want %q", got, gololang.Language) | |
| 51 | + } | |
| 52 | + if spans := syntax.Highlight(gololang.Language, "function main = |args| {"); len(spans[0]) == 0 { | |
| 53 | + t.Error("the registered Golo scanner colours nothing") | |
| 54 | + } | |
| 55 | +} | |
| 56 | + | |
| 57 | +func TestAScriptWithAShebangAndNoExtensionIsGoloToo(t *testing.T) { | |
| 58 | + // A script run as a command has no extension; its first line is what | |
| 59 | + // identifies it, and the editor reads that line before choosing a scanner. | |
| 60 | + root := t.TempDir() | |
| 61 | + path := filepath.Join(root, "greet") | |
| 62 | + writeFile(t, path, "#!/usr/bin/env golo\nmodule Greet\n\nfunction main = |args| {\n println(\"hi\")\n}\n") | |
| 63 | + | |
| 64 | + editor := newTestEditor(t) | |
| 65 | + editor.Open(path) | |
| 66 | + | |
| 67 | + if got := editor.ActiveView().Language(); got != gololang.Language { | |
| 68 | + t.Errorf("a script opening with a golo shebang is coloured as %q, want %q", got, gololang.Language) | |
| 69 | + } | |
| 70 | +} | |
| 71 | + | |
| 72 | +func TestTheEditorDoesNotColourMoonBit(t *testing.T) { | |
| 73 | + // "Golo instead of MoonBit" is the whole point of this editor being a | |
| 74 | + // separate one: a .mbt file opens as plain text here. | |
| 75 | + root := t.TempDir() | |
| 76 | + path := filepath.Join(root, "main.mbt") | |
| 77 | + writeFile(t, path, "fn main {\n println(\"hi\")\n}\n") | |
| 78 | + | |
| 79 | + editor := newTestEditor(t) | |
| 80 | + editor.Open(path) | |
| 81 | + | |
| 82 | + if got := editor.ActiveView().Language(); got != syntax.LanguageNone { | |
| 83 | + t.Errorf("a .mbt file is coloured as %q; Turbo Golo registers Golo, not MoonBit", got) | |
| 84 | + } | |
| 85 | +} | |
| 86 | + | |
| 87 | +func TestAProjectsOwnFilesAreStillColouredByTheLibrary(t *testing.T) { | |
| 88 | + // A README, a compose file and a Dockerfile are what a Golo project is | |
| 89 | + // made of besides its scripts, and turbo-core colours all three without | |
| 90 | + // this editor doing anything. That the inherited languages survive | |
| 91 | + // registration is worth one test, because syntax.Register writes into | |
| 92 | + // package-level state. | |
| 93 | + root := t.TempDir() | |
| 94 | + editor := newTestEditor(t) | |
| 95 | + | |
| 96 | + for name, want := range map[string]syntax.Language{ | |
| 97 | + "README.md": syntax.LanguageMarkdown, | |
| 98 | + "compose.yaml": syntax.LanguageYAML, | |
| 99 | + "Dockerfile": syntax.LanguageDockerfile, | |
| 100 | + } { | |
| 101 | + path := filepath.Join(root, name) | |
| 102 | + writeFile(t, path, "# heading\n") | |
| 103 | + editor.Open(path) | |
| 104 | + | |
| 105 | + if got := editor.ActiveView().Language(); got != want { | |
| 106 | + t.Errorf("%s is coloured as %q, want %q", name, got, want) | |
| 107 | + } | |
| 108 | + } | |
| 109 | +} | |
| 110 | + | |
| 111 | +func TestTheToolchainMenuIsCalledGoloAndNoTwoMenusShareAHotKey(t *testing.T) { | |
| 112 | + // The bar answers the first menu whose hot key matches, so a clash makes | |
| 113 | + // one of the two unreachable from the keyboard — silently, and with every | |
| 114 | + // other test still passing. Golo takes G because none of the fixed menus | |
| 115 | + // does, which is exactly the sort of thing only this test notices. | |
| 116 | + editor := newTestEditor(t) | |
| 117 | + | |
| 118 | + seen := map[rune]string{} | |
| 119 | + found := false | |
| 120 | + for _, menu := range editor.MenuBar().Menus() { | |
| 121 | + label, hot, _ := ui.SplitHotKey(menu.Label) | |
| 122 | + if label == "Golo" { | |
| 123 | + found = true | |
| 124 | + } | |
| 125 | + if hot == 0 { | |
| 126 | + t.Errorf("the %q menu has no hot key", label) | |
| 127 | + continue | |
| 128 | + } | |
| 129 | + if other, clash := seen[hot]; clash { | |
| 130 | + t.Errorf("%q and %q both answer to Alt-%c", other, label, hot) | |
| 131 | + } | |
| 132 | + seen[hot] = label | |
| 133 | + } | |
| 134 | + if !found { | |
| 135 | + t.Error("there is no Golo menu on the bar") | |
| 136 | + } | |
| 137 | +} | |
| 138 | + | |
| 139 | +// --- driven against a real golo lsp ----------------------------------------- | |
| 140 | + | |
| 141 | +// TestCompletionEndToEndWithRealGoloLSP drives the exact sequence the command | |
| 142 | +// does at start-up: open the file first, start the language server second, | |
| 143 | +// then ask for a completion. | |
| 144 | +// | |
| 145 | +// That order is the whole point, and it is the one Turbo Go got wrong once: an | |
| 146 | +// editor that announces its open documents to a server which does not exist yet | |
| 147 | +// and never mentions them again gets answers about a file the server has never | |
| 148 | +// heard of — which looks, from the outside, exactly like completion not | |
| 149 | +// working. | |
| 150 | +// | |
| 151 | +// It skips itself when golo is not installed, and under -short. | |
| 152 | +func TestCompletionEndToEndWithRealGoloLSP(t *testing.T) { | |
| 153 | + root, editor := startRealServer(t) | |
| 154 | + path := filepath.Join(root, "main.golo") | |
| 155 | + | |
| 156 | + // Both lines on disk are blank. A function is *declared* by typing, at top | |
| 157 | + // level, then its name is typed inside main, so the answer can only come from | |
| 158 | + // what the editor told the server — which is the whole point of this | |
| 159 | + // test. golo lsp offers keywords and builtins for any file at all, so a | |
| 160 | + // completion holding println would prove nothing; a completion holding a | |
| 161 | + // function that exists only in the buffer proves the buffer was sent. | |
| 162 | + view := editor.ActiveView() | |
| 163 | + view.Buffer().SetCursor(buffer.Position{Line: declarationLine, Col: 0}) | |
| 164 | + typeText(editor, "function zorglub = |x| -> x + 1") | |
| 165 | + view.Buffer().SetCursor(buffer.Position{Line: completionLine, Col: 2}) | |
| 166 | + typeText(editor, "zorg") | |
| 167 | + | |
| 168 | + if !waitForCompletion(t, editor) { | |
| 169 | + t.Fatalf("no completion list opened for %s; the status bar says %q", path, editor.StatusBar().Message()) | |
| 170 | + } | |
| 171 | + if !completionOffers(editor, "zorglub") { | |
| 172 | + t.Errorf("the list does not offer the function typed into the buffer; it has %d entries", editor.Completion().Count()) | |
| 173 | + } | |
| 174 | +} | |
| 175 | + | |
| 176 | +// The scanner's builtin table is read out of GoloScript rather than remembered, | |
| 177 | +// and this is where that claim is checked against the binary itself: the | |
| 178 | +// completion golo lsp offers for an empty prefix lists every keyword and every | |
| 179 | +// builtin it knows, so the two tables can be compared in both directions. | |
| 180 | +func TestTheScannersTablesMatchWhatTheServerOffers(t *testing.T) { | |
| 181 | + root, editor := startRealServer(t) | |
| 182 | + path := filepath.Join(root, "main.golo") | |
| 183 | + | |
| 184 | + var offered []lsp.CompletionItem | |
| 185 | + waitUntil(t, 30*time.Second, func() bool { | |
| 186 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) | |
| 187 | + defer cancel() | |
| 188 | + items, err := editor.Language().Complete(ctx, path, completionLine, 2, " ") | |
| 189 | + if err != nil { | |
| 190 | + return false | |
| 191 | + } | |
| 192 | + offered = items | |
| 193 | + return len(offered) > 0 | |
| 194 | + }) | |
| 195 | + | |
| 196 | + labels := map[string]bool{} | |
| 197 | + for _, item := range offered { | |
| 198 | + labels[item.Label] = true | |
| 199 | + } | |
| 200 | + | |
| 201 | + for _, builtin := range gololang.Builtins() { | |
| 202 | + if !labels[builtin] { | |
| 203 | + t.Errorf("the scanner colours %q as a builtin, and golo lsp does not offer it", builtin) | |
| 204 | + } | |
| 205 | + } | |
| 206 | + for _, keyword := range gololang.Keywords() { | |
| 207 | + if !labels[keyword] { | |
| 208 | + t.Errorf("the scanner colours %q as a keyword, and golo lsp does not offer it", keyword) | |
| 209 | + } | |
| 210 | + } | |
| 211 | + | |
| 212 | + // The other direction: everything the server offers that is not a keyword, | |
| 213 | + // a literal or a function the fixture declares must be in the builtin | |
| 214 | + // table, or the table has fallen behind the interpreter. | |
| 215 | + known := map[string]bool{"true": true, "false": true, "null": true} | |
| 216 | + for _, word := range append(gololang.Keywords(), gololang.Builtins()...) { | |
| 217 | + known[word] = true | |
| 218 | + } | |
| 219 | + for _, declared := range []string{"helper", "first", "second", "main"} { | |
| 220 | + known[declared] = true | |
| 221 | + } | |
| 222 | + for label := range labels { | |
| 223 | + if !known[label] { | |
| 224 | + t.Errorf("golo lsp offers %q, which the scanner knows nothing about", label) | |
| 225 | + } | |
| 226 | + } | |
| 227 | +} | |
| 228 | + | |
| 229 | +func TestGoToDefinitionWithRealGoloLSP(t *testing.T) { | |
| 230 | + root, editor := startRealServer(t) | |
| 231 | + path := filepath.Join(root, "main.golo") | |
| 232 | + | |
| 233 | + locations := waitForLocations(t, func(ctx context.Context) ([]lsp.Location, error) { | |
| 234 | + return editor.Language().Definition(ctx, path, callLine, callColumn, callLineText) | |
| 235 | + }) | |
| 236 | + | |
| 237 | + if len(locations) != 1 { | |
| 238 | + t.Fatalf("the call to helper has %d definitions, want exactly 1: %v", len(locations), locations) | |
| 239 | + } | |
| 240 | + if got := locations[0].Range.Start.Line; got != helperLine { | |
| 241 | + t.Errorf("the definition of helper is on line %d, want %d", got, helperLine) | |
| 242 | + } | |
| 243 | +} | |
| 244 | + | |
| 245 | +func TestHoverShowsTheCommentAboveADeclarationWithRealGoloLSP(t *testing.T) { | |
| 246 | + // A block of # comments above a declaration is its documentation, and the | |
| 247 | + // server shows it on hover. This is what F1 — Code ▸ Describe symbol — | |
| 248 | + // draws, and it is the one answer here that carries prose a person wrote. | |
| 249 | + root, editor := startRealServer(t) | |
| 250 | + path := filepath.Join(root, "main.golo") | |
| 251 | + | |
| 252 | + var text string | |
| 253 | + waitUntil(t, 30*time.Second, func() bool { | |
| 254 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) | |
| 255 | + defer cancel() | |
| 256 | + answer, err := editor.Language().Hover(ctx, path, callLine, callColumn, callLineText) | |
| 257 | + if err != nil { | |
| 258 | + return false | |
| 259 | + } | |
| 260 | + text = answer | |
| 261 | + return text != "" | |
| 262 | + }) | |
| 263 | + | |
| 264 | + if !strings.Contains(text, "Adds one") { | |
| 265 | + t.Errorf("hovering helper gave %q, want the comment written above its declaration", text) | |
| 266 | + } | |
| 267 | +} | |
| 268 | + | |
| 269 | +func TestTheSymbolsOfAFileWithRealGoloLSP(t *testing.T) { | |
| 270 | + root, editor := startRealServer(t) | |
| 271 | + path := filepath.Join(root, "main.golo") | |
| 272 | + | |
| 273 | + var symbols []lsp.Symbol | |
| 274 | + waitUntil(t, 30*time.Second, func() bool { | |
| 275 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) | |
| 276 | + defer cancel() | |
| 277 | + found, err := editor.Language().DocumentSymbols(ctx, path) | |
| 278 | + if err != nil { | |
| 279 | + return false | |
| 280 | + } | |
| 281 | + symbols = found | |
| 282 | + return len(symbols) > 0 | |
| 283 | + }) | |
| 284 | + | |
| 285 | + names := map[string]bool{} | |
| 286 | + for _, symbol := range symbols { | |
| 287 | + names[symbol.Name] = true | |
| 288 | + } | |
| 289 | + for _, want := range []string{"helper", "first", "second", "main"} { | |
| 290 | + if !names[want] { | |
| 291 | + t.Errorf("the file's symbols do not include %q: %v", want, names) | |
| 292 | + } | |
| 293 | + } | |
| 294 | +} | |
| 295 | + | |
| 296 | +// Diagnostics are the one thing a language server sends without being asked, | |
| 297 | +// and the only feature whose failure looks exactly like success: an editor with | |
| 298 | +// no error to show and one that cannot find the error are the same blank | |
| 299 | +// gutter. So this opens a file that does not parse and waits for the mark. | |
| 300 | +func TestDiagnosticsForAFileThatDoesNotParseWithRealGoloLSP(t *testing.T) { | |
| 301 | + root, editor := startRealServerOn(t, brokenScript) | |
| 302 | + path := filepath.Join(root, "main.golo") | |
| 303 | + | |
| 304 | + waitUntil(t, 30*time.Second, func() bool { | |
| 305 | + editor.Tick() | |
| 306 | + return len(editor.Language().Diagnostics(path)) > 0 | |
| 307 | + }) | |
| 308 | + | |
| 309 | + problems := editor.Language().Diagnostics(path) | |
| 310 | + if len(problems) == 0 { | |
| 311 | + t.Fatalf("no diagnostic ever arrived for %s; the status bar says %q", path, editor.StatusBar().Message()) | |
| 312 | + } | |
| 313 | + if _, ok := editor.Language().FirstError(path); !ok { | |
| 314 | + t.Errorf("the diagnostics hold no error, only %v", problems) | |
| 315 | + } | |
| 316 | +} | |
| 317 | + | |
| 318 | +// A C-style comment is the mistake everybody coming from another language | |
| 319 | +// makes, and golo lsp lints it rather than merely failing to parse it. It is | |
| 320 | +// the diagnostic a new Golo programmer meets first, so it is the one checked | |
| 321 | +// by name. | |
| 322 | +func TestACStyleCommentIsDiagnosedWithRealGoloLSP(t *testing.T) { | |
| 323 | + root, editor := startRealServerOn(t, "module demo.Lint\n\n// not a Golo comment\nfunction main = |args| {\n println(\"hi\")\n}\n") | |
| 324 | + path := filepath.Join(root, "main.golo") | |
| 325 | + | |
| 326 | + waitUntil(t, 30*time.Second, func() bool { | |
| 327 | + editor.Tick() | |
| 328 | + return len(editor.Language().Diagnostics(path)) > 0 | |
| 329 | + }) | |
| 330 | + | |
| 331 | + var messages []string | |
| 332 | + for _, problem := range editor.Language().Diagnostics(path) { | |
| 333 | + messages = append(messages, problem.Message) | |
| 334 | + } | |
| 335 | + if !slices.ContainsFunc(messages, func(m string) bool { return strings.Contains(m, "#") }) { | |
| 336 | + t.Errorf("the C-style comment was not diagnosed as one; the server said %v", messages) | |
| 337 | + } | |
| 338 | +} | |
| 339 | + | |
| 340 | +// golo lsp advertises neither referencesProvider, typeDefinitionProvider, | |
| 341 | +// implementationProvider nor workspaceSymbolProvider, so four of the nine | |
| 342 | +// questions turbo-core asks come back empty. That is documented in | |
| 343 | +// how-to/enable-completion.md, and this test is what keeps the documentation | |
| 344 | +// honest: if a future golo answers any of them, this fails and the page gets | |
| 345 | +// revisited. | |
| 346 | +func TestFindReferencesWithRealGoloLSP(t *testing.T) { | |
| 347 | + // GoloScript v0.2.0 started answering references. Asked from a call, the | |
| 348 | + // answer is the declaration and every call within the file; a use in | |
| 349 | + // another file of the same project is not found, because the server | |
| 350 | + // resolves nothing across files. | |
| 351 | + root, editor := startRealServer(t) | |
| 352 | + path := filepath.Join(root, "main.golo") | |
| 353 | + | |
| 354 | + locations := waitForLocations(t, func(ctx context.Context) ([]lsp.Location, error) { | |
| 355 | + return editor.Language().References(ctx, path, callLine, callColumn, callLineText) | |
| 356 | + }) | |
| 357 | + | |
| 358 | + lines := map[int]bool{} | |
| 359 | + for _, location := range locations { | |
| 360 | + if !strings.HasSuffix(location.URI, "/main.golo") { | |
| 361 | + t.Errorf("a reference points outside the file: %v", location) | |
| 362 | + } | |
| 363 | + lines[location.Range.Start.Line] = true | |
| 364 | + } | |
| 365 | + for _, want := range []int{helperLine, callLine, secondCallLine} { | |
| 366 | + if !lines[want] { | |
| 367 | + t.Errorf("the references to helper miss line %d: %v", want, locations) | |
| 368 | + } | |
| 369 | + } | |
| 370 | + if len(locations) != 3 { | |
| 371 | + t.Errorf("helper has %d references, want 3 (the declaration and two calls): %v", len(locations), locations) | |
| 372 | + } | |
| 373 | +} | |
| 374 | + | |
| 375 | +func TestFindImplementationsWithRealGoloLSP(t *testing.T) { | |
| 376 | + // GoloScript v0.2.0 started answering implementations, with the function's | |
| 377 | + // declaration: Golo has no interfaces, so a function is its own | |
| 378 | + // implementation, and the answer is the same place F12 goes. | |
| 379 | + root, editor := startRealServer(t) | |
| 380 | + path := filepath.Join(root, "main.golo") | |
| 381 | + | |
| 382 | + locations := waitForLocations(t, func(ctx context.Context) ([]lsp.Location, error) { | |
| 383 | + return editor.Language().Implementation(ctx, path, callLine, callColumn, callLineText) | |
| 384 | + }) | |
| 385 | + | |
| 386 | + if len(locations) != 1 { | |
| 387 | + t.Fatalf("the call to helper has %d implementations, want exactly 1: %v", len(locations), locations) | |
| 388 | + } | |
| 389 | + if got := locations[0].Range.Start.Line; got != helperLine { | |
| 390 | + t.Errorf("the implementation of helper is on line %d, want its declaration on %d", got, helperLine) | |
| 391 | + } | |
| 392 | +} | |
| 393 | + | |
| 394 | +func TestSymbolsAcrossTheProjectWithRealGoloLSP(t *testing.T) { | |
| 395 | + // GoloScript v0.2.0 started answering workspace/symbol. It searches every | |
| 396 | + // .golo file under the root, not only the ones the editor has opened, so | |
| 397 | + // the second file here is written and never announced to the server. | |
| 398 | + root, editor := startRealServer(t) | |
| 399 | + writeFile(t, filepath.Join(root, "other.golo"), "module demo.Other\n\nfunction elsewhere = |x| {\n return x\n}\n") | |
| 400 | + | |
| 401 | + find := func(query string) []lsp.Symbol { | |
| 402 | + var symbols []lsp.Symbol | |
| 403 | + waitUntil(t, 30*time.Second, func() bool { | |
| 404 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) | |
| 405 | + defer cancel() | |
| 406 | + found, err := editor.Language().WorkspaceSymbols(ctx, query) | |
| 407 | + if err != nil { | |
| 408 | + return false | |
| 409 | + } | |
| 410 | + symbols = found | |
| 411 | + return len(symbols) > 0 | |
| 412 | + }) | |
| 413 | + return symbols | |
| 414 | + } | |
| 415 | + | |
| 416 | + if symbols := find("helper"); len(symbols) == 0 || symbols[0].Name != "helper" { | |
| 417 | + t.Errorf("searching the project for helper gave %v", symbols) | |
| 418 | + } | |
| 419 | + if symbols := find("elsewhere"); len(symbols) == 0 || symbols[0].Name != "elsewhere" { | |
| 420 | + t.Errorf("searching the project for a function in a file the editor never opened gave %v", symbols) | |
| 421 | + } | |
| 422 | +} | |
| 423 | + | |
| 424 | +func TestGoloLSPDoesNotAnswerTypeDefinitionWithRealGoloLSP(t *testing.T) { | |
| 425 | + // The one question of the Code menu the server does not advertise. The | |
| 426 | + // documentation says so, and this fails the day a future golo answers it — | |
| 427 | + // which is how the three tests above came to exist: until GoloScript | |
| 428 | + // v0.2.0 references, implementations and project-wide symbols were refused | |
| 429 | + // too, and the test that pinned all four went red on 2026-09-19. | |
| 430 | + root, editor := startRealServer(t) | |
| 431 | + path := filepath.Join(root, "main.golo") | |
| 432 | + | |
| 433 | + ctx, cancel := context.WithTimeout(t.Context(), 15*time.Second) | |
| 434 | + defer cancel() | |
| 435 | + | |
| 436 | + if found, err := editor.Language().TypeDefinition(ctx, path, helperLine, helperColumn, helperLineText); err == nil && len(found) > 0 { | |
| 437 | + t.Errorf("golo lsp now answers type definitions (%v); how-to/enable-completion.md says it does not", found) | |
| 438 | + } | |
| 439 | +} | |
| 440 | + | |
| 441 | +// --- the fixtures and the waiting ------------------------------------------- | |
| 442 | + | |
| 443 | +// realScript is the file every language-server test works against. Line | |
| 444 | +// numbers are counted from zero and are named by the constants below, so | |
| 445 | +// inserting a line here moves them and the constants have to move too. | |
| 446 | +// | |
| 447 | +// 0 module demo.Main | |
| 448 | +// 1 | |
| 449 | +// 2 # Adds one. | |
| 450 | +// 3 function helper = |x| { | |
| 451 | +// 4 return x + 1 | |
| 452 | +// 5 } | |
| 453 | +// 6 | |
| 454 | +// 7 function first = |x| { | |
| 455 | +// 8 return helper(x) | |
| 456 | +// 9 } | |
| 457 | +// 10 | |
| 458 | +// 11 function second = |x| { | |
| 459 | +// 12 return helper(x) * 2 | |
| 460 | +// 13 } | |
| 461 | +// 14 ← where the declaration is typed, at top level: golo lsp offers only | |
| 462 | +// 15 top-level functions, so one typed inside main would never appear | |
| 463 | +// 16 function main = |args| { | |
| 464 | +// 17 let text = "hi" | |
| 465 | +// 18 ← two spaces, and where the completion is typed | |
| 466 | +// 19 println(first(1) + second(2) + text) | |
| 467 | +// 20 } | |
| 468 | +// | |
| 469 | +// It runs under golo with no error, which matters: a fixture the interpreter | |
| 470 | +// complains about would make the diagnostics test pass for the wrong reason. | |
| 471 | +const realScript = "module demo.Main\n" + | |
| 472 | + "\n" + | |
| 473 | + "# Adds one.\n" + | |
| 474 | + "function helper = |x| {\n" + | |
| 475 | + " return x + 1\n" + | |
| 476 | + "}\n" + | |
| 477 | + "\n" + | |
| 478 | + "function first = |x| {\n" + | |
| 479 | + " return helper(x)\n" + | |
| 480 | + "}\n" + | |
| 481 | + "\n" + | |
| 482 | + "function second = |x| {\n" + | |
| 483 | + " return helper(x) * 2\n" + | |
| 484 | + "}\n" + | |
| 485 | + "\n" + | |
| 486 | + "\n" + | |
| 487 | + "function main = |args| {\n" + | |
| 488 | + " let text = \"hi\"\n" + | |
| 489 | + " \n" + | |
| 490 | + " println(first(1) + second(2) + text)\n" + | |
| 491 | + "}\n" | |
| 492 | + | |
| 493 | +// brokenScript is a file that does not parse: the closing brace of main is | |
| 494 | +// missing. It exists as a second fixture rather than as a line added to the | |
| 495 | +// first, because a file holding a syntax error is a file whose *other* answers | |
| 496 | +// are worth nothing: the completion test would then be measuring a parse that | |
| 497 | +// never finished. | |
| 498 | +const brokenScript = "module demo.Broken\n" + | |
| 499 | + "\n" + | |
| 500 | + "function main = |args| {\n" + | |
| 501 | + " let x = (1 +\n" | |
| 502 | + | |
| 503 | +// Where the fixture's interesting lines are, counted from zero. | |
| 504 | +const ( | |
| 505 | + declarationLine = 14 | |
| 506 | + completionLine = 18 | |
| 507 | + helperLine = 3 | |
| 508 | + helperColumn = 9 | |
| 509 | + helperLineText = "function helper = |x| {" | |
| 510 | + callLine = 8 | |
| 511 | + callColumn = 9 | |
| 512 | + callLineText = " return helper(x)" | |
| 513 | + secondCallLine = 12 | |
| 514 | +) | |
| 515 | + | |
| 516 | +// startRealServer writes a script, opens it, starts golo lsp and waits for it, | |
| 517 | +// in the order the command does. It skips the test when golo is missing. | |
| 518 | +func startRealServer(t *testing.T) (root string, editor *app.App) { | |
| 519 | + t.Helper() | |
| 520 | + return startRealServerOn(t, realScript) | |
| 521 | +} | |
| 522 | + | |
| 523 | +// startRealServerOn is startRealServer over a chosen main.golo. | |
| 524 | +func startRealServerOn(t *testing.T, source string) (root string, editor *app.App) { | |
| 525 | + t.Helper() | |
| 526 | + if testing.Short() { | |
| 527 | + t.Skip("-short: not starting a language server") | |
| 528 | + } | |
| 529 | + | |
| 530 | + server, err := lsp.FindServer(gololang.Profile().Server) | |
| 531 | + if errors.Is(err, lsp.ErrServerNotFound) { | |
| 532 | + t.Skipf("%s is not installed; %s", gololang.ServerCommand, gololang.InstallHint) | |
| 533 | + } | |
| 534 | + // Finding it is not the same as being able to run it: a shim left behind by | |
| 535 | + // a tool manager whose environment has since been removed is on PATH and | |
| 536 | + // fails only when started. | |
| 537 | + if !serverRuns(server) { | |
| 538 | + t.Skipf("%s at %s cannot run; %s", gololang.ServerCommand, server, gololang.InstallHint) | |
| 539 | + } | |
| 540 | + | |
| 541 | + root = t.TempDir() | |
| 542 | + writeFile(t, filepath.Join(root, "main.golo"), source) | |
| 543 | + | |
| 544 | + editor = newTestEditor(t) | |
| 545 | + | |
| 546 | + // 1. Open the file, exactly as main does — before there is any server. | |
| 547 | + editor.Open(filepath.Join(root, "main.golo")) | |
| 548 | + | |
| 549 | + // 2. Start the language server, exactly as main does — afterwards, in the | |
| 550 | + // file's own directory, which is what ProjectRoot answers with no | |
| 551 | + // markers. | |
| 552 | + ctx, cancel := context.WithCancel(t.Context()) | |
| 553 | + t.Cleanup(cancel) | |
| 554 | + editor.StartLanguageServer(ctx, root) | |
| 555 | + t.Cleanup(func() { editor.Language().Stop(context.Background()) }) | |
| 556 | + | |
| 557 | + waitUntilReady(t, editor) | |
| 558 | + | |
| 559 | + // 3. Let the event loop notice the server is ready, as Run does on every | |
| 560 | + // turn. This is what announces the file that was already open. | |
| 561 | + editor.Tick() | |
| 562 | + return root, editor | |
| 563 | +} | |
| 564 | + | |
| 565 | +// newTestEditor returns Turbo Golo drawing on a simulated terminal, set up the | |
| 566 | +// way the command sets it up. | |
| 567 | +func newTestEditor(t *testing.T) *app.App { | |
| 568 | + t.Helper() | |
| 569 | + | |
| 570 | + gololang.Register() | |
| 571 | + screen := tcell.NewSimulationScreen("UTF-8") | |
| 572 | + if err := screen.Init(); err != nil { | |
| 573 | + t.Fatalf("initialising the simulation screen: %v", err) | |
| 574 | + } | |
| 575 | + t.Cleanup(screen.Fini) | |
| 576 | + screen.SetSize(80, 24) | |
| 577 | + | |
| 578 | + // Never read the themes or snippets of whoever is running the tests. | |
| 579 | + p := gololang.Profile() | |
| 580 | + t.Setenv(p.ThemeDirEnvVar(), t.TempDir()) | |
| 581 | + t.Setenv(p.SnippetDirEnvVar(), t.TempDir()) | |
| 582 | + | |
| 583 | + editor := app.New(screen, "turbo-classic", p) | |
| 584 | + editor.Render() | |
| 585 | + return editor | |
| 586 | +} | |
| 587 | + | |
| 588 | +// typeText sends a run of printable characters through the whole routing chain. | |
| 589 | +func typeText(editor *app.App, text string) { | |
| 590 | + for _, r := range text { | |
| 591 | + editor.Handle(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone)) | |
| 592 | + } | |
| 593 | +} | |
| 594 | + | |
| 595 | +// completionOffers reports whether the open popup holds an entry starting with | |
| 596 | +// a label. | |
| 597 | +func completionOffers(editor *app.App, label string) bool { | |
| 598 | + for _, item := range editor.Completion().Matches() { | |
| 599 | + if strings.HasPrefix(item.Label, label) { | |
| 600 | + return true | |
| 601 | + } | |
| 602 | + } | |
| 603 | + return false | |
| 604 | +} | |
| 605 | + | |
| 606 | +// waitUntilReady blocks until the language server has finished starting. | |
| 607 | +func waitUntilReady(t *testing.T, editor *app.App) { | |
| 608 | + t.Helper() | |
| 609 | + | |
| 610 | + deadline := time.After(lsp.InitializeTimeout) | |
| 611 | + for !editor.Language().Ready() { | |
| 612 | + select { | |
| 613 | + case <-deadline: | |
| 614 | + t.Fatalf("the language server never became ready: %s", editor.Language().Status()) | |
| 615 | + case <-time.After(10 * time.Millisecond): | |
| 616 | + } | |
| 617 | + } | |
| 618 | +} | |
| 619 | + | |
| 620 | +// waitUntil polls a condition until it holds or the time runs out, and fails | |
| 621 | +// the test if it never does. | |
| 622 | +func waitUntil(t *testing.T, within time.Duration, done func() bool) { | |
| 623 | + t.Helper() | |
| 624 | + | |
| 625 | + deadline := time.Now().Add(within) | |
| 626 | + for time.Now().Before(deadline) { | |
| 627 | + if done() { | |
| 628 | + return | |
| 629 | + } | |
| 630 | + time.Sleep(200 * time.Millisecond) | |
| 631 | + } | |
| 632 | + t.Errorf("the server never answered within %s", within) | |
| 633 | +} | |
| 634 | + | |
| 635 | +// waitForLocations asks a location question until it is answered, because a | |
| 636 | +// server that is still indexing answers an empty list rather than an error. | |
| 637 | +func waitForLocations(t *testing.T, ask func(context.Context) ([]lsp.Location, error)) []lsp.Location { | |
| 638 | + t.Helper() | |
| 639 | + | |
| 640 | + var found []lsp.Location | |
| 641 | + waitUntil(t, 30*time.Second, func() bool { | |
| 642 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) | |
| 643 | + defer cancel() | |
| 644 | + | |
| 645 | + locations, err := ask(ctx) | |
| 646 | + if err != nil { | |
| 647 | + return false | |
| 648 | + } | |
| 649 | + found = locations | |
| 650 | + return len(found) > 0 | |
| 651 | + }) | |
| 652 | + return found | |
| 653 | +} | |
| 654 | + | |
| 655 | +// waitForCompletion asks for a completion until one arrives, or gives up. | |
| 656 | +// | |
| 657 | +// A server may load its state after it has finished initialising, and answer | |
| 658 | +// an empty list until that is done. There is no notification this client reads | |
| 659 | +// that says when — so it asks again, which is what the editor's user would do. | |
| 660 | +func waitForCompletion(t *testing.T, editor *app.App) bool { | |
| 661 | + t.Helper() | |
| 662 | + | |
| 663 | + deadline := time.Now().Add(60 * time.Second) | |
| 664 | + for time.Now().Before(deadline) { | |
| 665 | + if editor.Completion().Visible() { | |
| 666 | + return true | |
| 667 | + } | |
| 668 | + editor.RequestCompletion() | |
| 669 | + if editor.Completion().Visible() { | |
| 670 | + return true | |
| 671 | + } | |
| 672 | + time.Sleep(500 * time.Millisecond) | |
| 673 | + } | |
| 674 | + return false | |
| 675 | +} | |
| 676 | + | |
| 677 | +// serverRuns reports whether the language server at path actually starts. | |
| 678 | +func serverRuns(path string) bool { | |
| 679 | + return exec.Command(path, "--version").Run() == nil | |
| 680 | +} | |
| 681 | + | |
| 682 | +// writeFile creates a file, making its directory first. | |
| 683 | +func writeFile(t *testing.T, path, content string) { | |
| 684 | + t.Helper() | |
| 685 | + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { | |
| 686 | + t.Fatalf("creating %s: %v", filepath.Dir(path), err) | |
| 687 | + } | |
| 688 | + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { | |
| 689 | + t.Fatalf("writing %s: %v", path, err) | |
| 690 | + } | |
| 691 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,691 @@ | |||
| 1 | +package gololang_test | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + "errors" | ||
| 6 | + "os" | ||
| 7 | + "os/exec" | ||
| 8 | + "path/filepath" | ||
| 9 | + "slices" | ||
| 10 | + "strings" | ||
| 11 | + "testing" | ||
| 12 | + "time" | ||
| 13 | + | ||
| 14 | + "github.com/gdamore/tcell/v2" | ||
| 15 | + | ||
| 16 | + "rickub.com/turbo-editors/turbo-core/app" | ||
| 17 | + "rickub.com/turbo-editors/turbo-core/buffer" | ||
| 18 | + "rickub.com/turbo-editors/turbo-core/lsp" | ||
| 19 | + "rickub.com/turbo-editors/turbo-core/syntax" | ||
| 20 | + "rickub.com/turbo-editors/turbo-core/ui" | ||
| 21 | + | ||
| 22 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | ||
| 23 | +) | ||
| 24 | + | ||
| 25 | +// --- the editor, assembled -------------------------------------------------- | ||
| 26 | + | ||
| 27 | +func TestTheEditorCallsItselfTurboGolo(t *testing.T) { | ||
| 28 | + editor := newTestEditor(t) | ||
| 29 | + | ||
| 30 | + if got := editor.Profile().Name; got != gololang.Name { | ||
| 31 | + t.Errorf("Profile().Name = %q, want %q", got, gololang.Name) | ||
| 32 | + } | ||
| 33 | + if got := editor.Profile().ProjectDir(); got != ".turbo-golo" { | ||
| 34 | + t.Errorf("ProjectDir() = %q, want %q", got, ".turbo-golo") | ||
| 35 | + } | ||
| 36 | +} | ||
| 37 | + | ||
| 38 | +func TestTheEditorColoursGoloSourceItOpens(t *testing.T) { | ||
| 39 | + // The whole path in one test: Register taught the library about Golo, the | ||
| 40 | + // profile named the editor, and a .golo file opened through the public | ||
| 41 | + // API comes out coloured. | ||
| 42 | + root := t.TempDir() | ||
| 43 | + path := filepath.Join(root, "main.golo") | ||
| 44 | + writeFile(t, path, "module demo.Main\n\nfunction main = |args| {\n println(\"hi\")\n}\n") | ||
| 45 | + | ||
| 46 | + editor := newTestEditor(t) | ||
| 47 | + editor.Open(path) | ||
| 48 | + | ||
| 49 | + if got := editor.ActiveView().Language(); got != gololang.Language { | ||
| 50 | + t.Fatalf("the view colours the file as %q, want %q", got, gololang.Language) | ||
| 51 | + } | ||
| 52 | + if spans := syntax.Highlight(gololang.Language, "function main = |args| {"); len(spans[0]) == 0 { | ||
| 53 | + t.Error("the registered Golo scanner colours nothing") | ||
| 54 | + } | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +func TestAScriptWithAShebangAndNoExtensionIsGoloToo(t *testing.T) { | ||
| 58 | + // A script run as a command has no extension; its first line is what | ||
| 59 | + // identifies it, and the editor reads that line before choosing a scanner. | ||
| 60 | + root := t.TempDir() | ||
| 61 | + path := filepath.Join(root, "greet") | ||
| 62 | + writeFile(t, path, "#!/usr/bin/env golo\nmodule Greet\n\nfunction main = |args| {\n println(\"hi\")\n}\n") | ||
| 63 | + | ||
| 64 | + editor := newTestEditor(t) | ||
| 65 | + editor.Open(path) | ||
| 66 | + | ||
| 67 | + if got := editor.ActiveView().Language(); got != gololang.Language { | ||
| 68 | + t.Errorf("a script opening with a golo shebang is coloured as %q, want %q", got, gololang.Language) | ||
| 69 | + } | ||
| 70 | +} | ||
| 71 | + | ||
| 72 | +func TestTheEditorDoesNotColourMoonBit(t *testing.T) { | ||
| 73 | + // "Golo instead of MoonBit" is the whole point of this editor being a | ||
| 74 | + // separate one: a .mbt file opens as plain text here. | ||
| 75 | + root := t.TempDir() | ||
| 76 | + path := filepath.Join(root, "main.mbt") | ||
| 77 | + writeFile(t, path, "fn main {\n println(\"hi\")\n}\n") | ||
| 78 | + | ||
| 79 | + editor := newTestEditor(t) | ||
| 80 | + editor.Open(path) | ||
| 81 | + | ||
| 82 | + if got := editor.ActiveView().Language(); got != syntax.LanguageNone { | ||
| 83 | + t.Errorf("a .mbt file is coloured as %q; Turbo Golo registers Golo, not MoonBit", got) | ||
| 84 | + } | ||
| 85 | +} | ||
| 86 | + | ||
| 87 | +func TestAProjectsOwnFilesAreStillColouredByTheLibrary(t *testing.T) { | ||
| 88 | + // A README, a compose file and a Dockerfile are what a Golo project is | ||
| 89 | + // made of besides its scripts, and turbo-core colours all three without | ||
| 90 | + // this editor doing anything. That the inherited languages survive | ||
| 91 | + // registration is worth one test, because syntax.Register writes into | ||
| 92 | + // package-level state. | ||
| 93 | + root := t.TempDir() | ||
| 94 | + editor := newTestEditor(t) | ||
| 95 | + | ||
| 96 | + for name, want := range map[string]syntax.Language{ | ||
| 97 | + "README.md": syntax.LanguageMarkdown, | ||
| 98 | + "compose.yaml": syntax.LanguageYAML, | ||
| 99 | + "Dockerfile": syntax.LanguageDockerfile, | ||
| 100 | + } { | ||
| 101 | + path := filepath.Join(root, name) | ||
| 102 | + writeFile(t, path, "# heading\n") | ||
| 103 | + editor.Open(path) | ||
| 104 | + | ||
| 105 | + if got := editor.ActiveView().Language(); got != want { | ||
| 106 | + t.Errorf("%s is coloured as %q, want %q", name, got, want) | ||
| 107 | + } | ||
| 108 | + } | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +func TestTheToolchainMenuIsCalledGoloAndNoTwoMenusShareAHotKey(t *testing.T) { | ||
| 112 | + // The bar answers the first menu whose hot key matches, so a clash makes | ||
| 113 | + // one of the two unreachable from the keyboard — silently, and with every | ||
| 114 | + // other test still passing. Golo takes G because none of the fixed menus | ||
| 115 | + // does, which is exactly the sort of thing only this test notices. | ||
| 116 | + editor := newTestEditor(t) | ||
| 117 | + | ||
| 118 | + seen := map[rune]string{} | ||
| 119 | + found := false | ||
| 120 | + for _, menu := range editor.MenuBar().Menus() { | ||
| 121 | + label, hot, _ := ui.SplitHotKey(menu.Label) | ||
| 122 | + if label == "Golo" { | ||
| 123 | + found = true | ||
| 124 | + } | ||
| 125 | + if hot == 0 { | ||
| 126 | + t.Errorf("the %q menu has no hot key", label) | ||
| 127 | + continue | ||
| 128 | + } | ||
| 129 | + if other, clash := seen[hot]; clash { | ||
| 130 | + t.Errorf("%q and %q both answer to Alt-%c", other, label, hot) | ||
| 131 | + } | ||
| 132 | + seen[hot] = label | ||
| 133 | + } | ||
| 134 | + if !found { | ||
| 135 | + t.Error("there is no Golo menu on the bar") | ||
| 136 | + } | ||
| 137 | +} | ||
| 138 | + | ||
| 139 | +// --- driven against a real golo lsp ----------------------------------------- | ||
| 140 | + | ||
| 141 | +// TestCompletionEndToEndWithRealGoloLSP drives the exact sequence the command | ||
| 142 | +// does at start-up: open the file first, start the language server second, | ||
| 143 | +// then ask for a completion. | ||
| 144 | +// | ||
| 145 | +// That order is the whole point, and it is the one Turbo Go got wrong once: an | ||
| 146 | +// editor that announces its open documents to a server which does not exist yet | ||
| 147 | +// and never mentions them again gets answers about a file the server has never | ||
| 148 | +// heard of — which looks, from the outside, exactly like completion not | ||
| 149 | +// working. | ||
| 150 | +// | ||
| 151 | +// It skips itself when golo is not installed, and under -short. | ||
| 152 | +func TestCompletionEndToEndWithRealGoloLSP(t *testing.T) { | ||
| 153 | + root, editor := startRealServer(t) | ||
| 154 | + path := filepath.Join(root, "main.golo") | ||
| 155 | + | ||
| 156 | + // Both lines on disk are blank. A function is *declared* by typing, at top | ||
| 157 | + // level, then its name is typed inside main, so the answer can only come from | ||
| 158 | + // what the editor told the server — which is the whole point of this | ||
| 159 | + // test. golo lsp offers keywords and builtins for any file at all, so a | ||
| 160 | + // completion holding println would prove nothing; a completion holding a | ||
| 161 | + // function that exists only in the buffer proves the buffer was sent. | ||
| 162 | + view := editor.ActiveView() | ||
| 163 | + view.Buffer().SetCursor(buffer.Position{Line: declarationLine, Col: 0}) | ||
| 164 | + typeText(editor, "function zorglub = |x| -> x + 1") | ||
| 165 | + view.Buffer().SetCursor(buffer.Position{Line: completionLine, Col: 2}) | ||
| 166 | + typeText(editor, "zorg") | ||
| 167 | + | ||
| 168 | + if !waitForCompletion(t, editor) { | ||
| 169 | + t.Fatalf("no completion list opened for %s; the status bar says %q", path, editor.StatusBar().Message()) | ||
| 170 | + } | ||
| 171 | + if !completionOffers(editor, "zorglub") { | ||
| 172 | + t.Errorf("the list does not offer the function typed into the buffer; it has %d entries", editor.Completion().Count()) | ||
| 173 | + } | ||
| 174 | +} | ||
| 175 | + | ||
| 176 | +// The scanner's builtin table is read out of GoloScript rather than remembered, | ||
| 177 | +// and this is where that claim is checked against the binary itself: the | ||
| 178 | +// completion golo lsp offers for an empty prefix lists every keyword and every | ||
| 179 | +// builtin it knows, so the two tables can be compared in both directions. | ||
| 180 | +func TestTheScannersTablesMatchWhatTheServerOffers(t *testing.T) { | ||
| 181 | + root, editor := startRealServer(t) | ||
| 182 | + path := filepath.Join(root, "main.golo") | ||
| 183 | + | ||
| 184 | + var offered []lsp.CompletionItem | ||
| 185 | + waitUntil(t, 30*time.Second, func() bool { | ||
| 186 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) | ||
| 187 | + defer cancel() | ||
| 188 | + items, err := editor.Language().Complete(ctx, path, completionLine, 2, " ") | ||
| 189 | + if err != nil { | ||
| 190 | + return false | ||
| 191 | + } | ||
| 192 | + offered = items | ||
| 193 | + return len(offered) > 0 | ||
| 194 | + }) | ||
| 195 | + | ||
| 196 | + labels := map[string]bool{} | ||
| 197 | + for _, item := range offered { | ||
| 198 | + labels[item.Label] = true | ||
| 199 | + } | ||
| 200 | + | ||
| 201 | + for _, builtin := range gololang.Builtins() { | ||
| 202 | + if !labels[builtin] { | ||
| 203 | + t.Errorf("the scanner colours %q as a builtin, and golo lsp does not offer it", builtin) | ||
| 204 | + } | ||
| 205 | + } | ||
| 206 | + for _, keyword := range gololang.Keywords() { | ||
| 207 | + if !labels[keyword] { | ||
| 208 | + t.Errorf("the scanner colours %q as a keyword, and golo lsp does not offer it", keyword) | ||
| 209 | + } | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + // The other direction: everything the server offers that is not a keyword, | ||
| 213 | + // a literal or a function the fixture declares must be in the builtin | ||
| 214 | + // table, or the table has fallen behind the interpreter. | ||
| 215 | + known := map[string]bool{"true": true, "false": true, "null": true} | ||
| 216 | + for _, word := range append(gololang.Keywords(), gololang.Builtins()...) { | ||
| 217 | + known[word] = true | ||
| 218 | + } | ||
| 219 | + for _, declared := range []string{"helper", "first", "second", "main"} { | ||
| 220 | + known[declared] = true | ||
| 221 | + } | ||
| 222 | + for label := range labels { | ||
| 223 | + if !known[label] { | ||
| 224 | + t.Errorf("golo lsp offers %q, which the scanner knows nothing about", label) | ||
| 225 | + } | ||
| 226 | + } | ||
| 227 | +} | ||
| 228 | + | ||
| 229 | +func TestGoToDefinitionWithRealGoloLSP(t *testing.T) { | ||
| 230 | + root, editor := startRealServer(t) | ||
| 231 | + path := filepath.Join(root, "main.golo") | ||
| 232 | + | ||
| 233 | + locations := waitForLocations(t, func(ctx context.Context) ([]lsp.Location, error) { | ||
| 234 | + return editor.Language().Definition(ctx, path, callLine, callColumn, callLineText) | ||
| 235 | + }) | ||
| 236 | + | ||
| 237 | + if len(locations) != 1 { | ||
| 238 | + t.Fatalf("the call to helper has %d definitions, want exactly 1: %v", len(locations), locations) | ||
| 239 | + } | ||
| 240 | + if got := locations[0].Range.Start.Line; got != helperLine { | ||
| 241 | + t.Errorf("the definition of helper is on line %d, want %d", got, helperLine) | ||
| 242 | + } | ||
| 243 | +} | ||
| 244 | + | ||
| 245 | +func TestHoverShowsTheCommentAboveADeclarationWithRealGoloLSP(t *testing.T) { | ||
| 246 | + // A block of # comments above a declaration is its documentation, and the | ||
| 247 | + // server shows it on hover. This is what F1 — Code ▸ Describe symbol — | ||
| 248 | + // draws, and it is the one answer here that carries prose a person wrote. | ||
| 249 | + root, editor := startRealServer(t) | ||
| 250 | + path := filepath.Join(root, "main.golo") | ||
| 251 | + | ||
| 252 | + var text string | ||
| 253 | + waitUntil(t, 30*time.Second, func() bool { | ||
| 254 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) | ||
| 255 | + defer cancel() | ||
| 256 | + answer, err := editor.Language().Hover(ctx, path, callLine, callColumn, callLineText) | ||
| 257 | + if err != nil { | ||
| 258 | + return false | ||
| 259 | + } | ||
| 260 | + text = answer | ||
| 261 | + return text != "" | ||
| 262 | + }) | ||
| 263 | + | ||
| 264 | + if !strings.Contains(text, "Adds one") { | ||
| 265 | + t.Errorf("hovering helper gave %q, want the comment written above its declaration", text) | ||
| 266 | + } | ||
| 267 | +} | ||
| 268 | + | ||
| 269 | +func TestTheSymbolsOfAFileWithRealGoloLSP(t *testing.T) { | ||
| 270 | + root, editor := startRealServer(t) | ||
| 271 | + path := filepath.Join(root, "main.golo") | ||
| 272 | + | ||
| 273 | + var symbols []lsp.Symbol | ||
| 274 | + waitUntil(t, 30*time.Second, func() bool { | ||
| 275 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) | ||
| 276 | + defer cancel() | ||
| 277 | + found, err := editor.Language().DocumentSymbols(ctx, path) | ||
| 278 | + if err != nil { | ||
| 279 | + return false | ||
| 280 | + } | ||
| 281 | + symbols = found | ||
| 282 | + return len(symbols) > 0 | ||
| 283 | + }) | ||
| 284 | + | ||
| 285 | + names := map[string]bool{} | ||
| 286 | + for _, symbol := range symbols { | ||
| 287 | + names[symbol.Name] = true | ||
| 288 | + } | ||
| 289 | + for _, want := range []string{"helper", "first", "second", "main"} { | ||
| 290 | + if !names[want] { | ||
| 291 | + t.Errorf("the file's symbols do not include %q: %v", want, names) | ||
| 292 | + } | ||
| 293 | + } | ||
| 294 | +} | ||
| 295 | + | ||
| 296 | +// Diagnostics are the one thing a language server sends without being asked, | ||
| 297 | +// and the only feature whose failure looks exactly like success: an editor with | ||
| 298 | +// no error to show and one that cannot find the error are the same blank | ||
| 299 | +// gutter. So this opens a file that does not parse and waits for the mark. | ||
| 300 | +func TestDiagnosticsForAFileThatDoesNotParseWithRealGoloLSP(t *testing.T) { | ||
| 301 | + root, editor := startRealServerOn(t, brokenScript) | ||
| 302 | + path := filepath.Join(root, "main.golo") | ||
| 303 | + | ||
| 304 | + waitUntil(t, 30*time.Second, func() bool { | ||
| 305 | + editor.Tick() | ||
| 306 | + return len(editor.Language().Diagnostics(path)) > 0 | ||
| 307 | + }) | ||
| 308 | + | ||
| 309 | + problems := editor.Language().Diagnostics(path) | ||
| 310 | + if len(problems) == 0 { | ||
| 311 | + t.Fatalf("no diagnostic ever arrived for %s; the status bar says %q", path, editor.StatusBar().Message()) | ||
| 312 | + } | ||
| 313 | + if _, ok := editor.Language().FirstError(path); !ok { | ||
| 314 | + t.Errorf("the diagnostics hold no error, only %v", problems) | ||
| 315 | + } | ||
| 316 | +} | ||
| 317 | + | ||
| 318 | +// A C-style comment is the mistake everybody coming from another language | ||
| 319 | +// makes, and golo lsp lints it rather than merely failing to parse it. It is | ||
| 320 | +// the diagnostic a new Golo programmer meets first, so it is the one checked | ||
| 321 | +// by name. | ||
| 322 | +func TestACStyleCommentIsDiagnosedWithRealGoloLSP(t *testing.T) { | ||
| 323 | + root, editor := startRealServerOn(t, "module demo.Lint\n\n// not a Golo comment\nfunction main = |args| {\n println(\"hi\")\n}\n") | ||
| 324 | + path := filepath.Join(root, "main.golo") | ||
| 325 | + | ||
| 326 | + waitUntil(t, 30*time.Second, func() bool { | ||
| 327 | + editor.Tick() | ||
| 328 | + return len(editor.Language().Diagnostics(path)) > 0 | ||
| 329 | + }) | ||
| 330 | + | ||
| 331 | + var messages []string | ||
| 332 | + for _, problem := range editor.Language().Diagnostics(path) { | ||
| 333 | + messages = append(messages, problem.Message) | ||
| 334 | + } | ||
| 335 | + if !slices.ContainsFunc(messages, func(m string) bool { return strings.Contains(m, "#") }) { | ||
| 336 | + t.Errorf("the C-style comment was not diagnosed as one; the server said %v", messages) | ||
| 337 | + } | ||
| 338 | +} | ||
| 339 | + | ||
| 340 | +// golo lsp advertises neither referencesProvider, typeDefinitionProvider, | ||
| 341 | +// implementationProvider nor workspaceSymbolProvider, so four of the nine | ||
| 342 | +// questions turbo-core asks come back empty. That is documented in | ||
| 343 | +// how-to/enable-completion.md, and this test is what keeps the documentation | ||
| 344 | +// honest: if a future golo answers any of them, this fails and the page gets | ||
| 345 | +// revisited. | ||
| 346 | +func TestFindReferencesWithRealGoloLSP(t *testing.T) { | ||
| 347 | + // GoloScript v0.2.0 started answering references. Asked from a call, the | ||
| 348 | + // answer is the declaration and every call within the file; a use in | ||
| 349 | + // another file of the same project is not found, because the server | ||
| 350 | + // resolves nothing across files. | ||
| 351 | + root, editor := startRealServer(t) | ||
| 352 | + path := filepath.Join(root, "main.golo") | ||
| 353 | + | ||
| 354 | + locations := waitForLocations(t, func(ctx context.Context) ([]lsp.Location, error) { | ||
| 355 | + return editor.Language().References(ctx, path, callLine, callColumn, callLineText) | ||
| 356 | + }) | ||
| 357 | + | ||
| 358 | + lines := map[int]bool{} | ||
| 359 | + for _, location := range locations { | ||
| 360 | + if !strings.HasSuffix(location.URI, "/main.golo") { | ||
| 361 | + t.Errorf("a reference points outside the file: %v", location) | ||
| 362 | + } | ||
| 363 | + lines[location.Range.Start.Line] = true | ||
| 364 | + } | ||
| 365 | + for _, want := range []int{helperLine, callLine, secondCallLine} { | ||
| 366 | + if !lines[want] { | ||
| 367 | + t.Errorf("the references to helper miss line %d: %v", want, locations) | ||
| 368 | + } | ||
| 369 | + } | ||
| 370 | + if len(locations) != 3 { | ||
| 371 | + t.Errorf("helper has %d references, want 3 (the declaration and two calls): %v", len(locations), locations) | ||
| 372 | + } | ||
| 373 | +} | ||
| 374 | + | ||
| 375 | +func TestFindImplementationsWithRealGoloLSP(t *testing.T) { | ||
| 376 | + // GoloScript v0.2.0 started answering implementations, with the function's | ||
| 377 | + // declaration: Golo has no interfaces, so a function is its own | ||
| 378 | + // implementation, and the answer is the same place F12 goes. | ||
| 379 | + root, editor := startRealServer(t) | ||
| 380 | + path := filepath.Join(root, "main.golo") | ||
| 381 | + | ||
| 382 | + locations := waitForLocations(t, func(ctx context.Context) ([]lsp.Location, error) { | ||
| 383 | + return editor.Language().Implementation(ctx, path, callLine, callColumn, callLineText) | ||
| 384 | + }) | ||
| 385 | + | ||
| 386 | + if len(locations) != 1 { | ||
| 387 | + t.Fatalf("the call to helper has %d implementations, want exactly 1: %v", len(locations), locations) | ||
| 388 | + } | ||
| 389 | + if got := locations[0].Range.Start.Line; got != helperLine { | ||
| 390 | + t.Errorf("the implementation of helper is on line %d, want its declaration on %d", got, helperLine) | ||
| 391 | + } | ||
| 392 | +} | ||
| 393 | + | ||
| 394 | +func TestSymbolsAcrossTheProjectWithRealGoloLSP(t *testing.T) { | ||
| 395 | + // GoloScript v0.2.0 started answering workspace/symbol. It searches every | ||
| 396 | + // .golo file under the root, not only the ones the editor has opened, so | ||
| 397 | + // the second file here is written and never announced to the server. | ||
| 398 | + root, editor := startRealServer(t) | ||
| 399 | + writeFile(t, filepath.Join(root, "other.golo"), "module demo.Other\n\nfunction elsewhere = |x| {\n return x\n}\n") | ||
| 400 | + | ||
| 401 | + find := func(query string) []lsp.Symbol { | ||
| 402 | + var symbols []lsp.Symbol | ||
| 403 | + waitUntil(t, 30*time.Second, func() bool { | ||
| 404 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) | ||
| 405 | + defer cancel() | ||
| 406 | + found, err := editor.Language().WorkspaceSymbols(ctx, query) | ||
| 407 | + if err != nil { | ||
| 408 | + return false | ||
| 409 | + } | ||
| 410 | + symbols = found | ||
| 411 | + return len(symbols) > 0 | ||
| 412 | + }) | ||
| 413 | + return symbols | ||
| 414 | + } | ||
| 415 | + | ||
| 416 | + if symbols := find("helper"); len(symbols) == 0 || symbols[0].Name != "helper" { | ||
| 417 | + t.Errorf("searching the project for helper gave %v", symbols) | ||
| 418 | + } | ||
| 419 | + if symbols := find("elsewhere"); len(symbols) == 0 || symbols[0].Name != "elsewhere" { | ||
| 420 | + t.Errorf("searching the project for a function in a file the editor never opened gave %v", symbols) | ||
| 421 | + } | ||
| 422 | +} | ||
| 423 | + | ||
| 424 | +func TestGoloLSPDoesNotAnswerTypeDefinitionWithRealGoloLSP(t *testing.T) { | ||
| 425 | + // The one question of the Code menu the server does not advertise. The | ||
| 426 | + // documentation says so, and this fails the day a future golo answers it — | ||
| 427 | + // which is how the three tests above came to exist: until GoloScript | ||
| 428 | + // v0.2.0 references, implementations and project-wide symbols were refused | ||
| 429 | + // too, and the test that pinned all four went red on 2026-09-19. | ||
| 430 | + root, editor := startRealServer(t) | ||
| 431 | + path := filepath.Join(root, "main.golo") | ||
| 432 | + | ||
| 433 | + ctx, cancel := context.WithTimeout(t.Context(), 15*time.Second) | ||
| 434 | + defer cancel() | ||
| 435 | + | ||
| 436 | + if found, err := editor.Language().TypeDefinition(ctx, path, helperLine, helperColumn, helperLineText); err == nil && len(found) > 0 { | ||
| 437 | + t.Errorf("golo lsp now answers type definitions (%v); how-to/enable-completion.md says it does not", found) | ||
| 438 | + } | ||
| 439 | +} | ||
| 440 | + | ||
| 441 | +// --- the fixtures and the waiting ------------------------------------------- | ||
| 442 | + | ||
| 443 | +// realScript is the file every language-server test works against. Line | ||
| 444 | +// numbers are counted from zero and are named by the constants below, so | ||
| 445 | +// inserting a line here moves them and the constants have to move too. | ||
| 446 | +// | ||
| 447 | +// 0 module demo.Main | ||
| 448 | +// 1 | ||
| 449 | +// 2 # Adds one. | ||
| 450 | +// 3 function helper = |x| { | ||
| 451 | +// 4 return x + 1 | ||
| 452 | +// 5 } | ||
| 453 | +// 6 | ||
| 454 | +// 7 function first = |x| { | ||
| 455 | +// 8 return helper(x) | ||
| 456 | +// 9 } | ||
| 457 | +// 10 | ||
| 458 | +// 11 function second = |x| { | ||
| 459 | +// 12 return helper(x) * 2 | ||
| 460 | +// 13 } | ||
| 461 | +// 14 ← where the declaration is typed, at top level: golo lsp offers only | ||
| 462 | +// 15 top-level functions, so one typed inside main would never appear | ||
| 463 | +// 16 function main = |args| { | ||
| 464 | +// 17 let text = "hi" | ||
| 465 | +// 18 ← two spaces, and where the completion is typed | ||
| 466 | +// 19 println(first(1) + second(2) + text) | ||
| 467 | +// 20 } | ||
| 468 | +// | ||
| 469 | +// It runs under golo with no error, which matters: a fixture the interpreter | ||
| 470 | +// complains about would make the diagnostics test pass for the wrong reason. | ||
| 471 | +const realScript = "module demo.Main\n" + | ||
| 472 | + "\n" + | ||
| 473 | + "# Adds one.\n" + | ||
| 474 | + "function helper = |x| {\n" + | ||
| 475 | + " return x + 1\n" + | ||
| 476 | + "}\n" + | ||
| 477 | + "\n" + | ||
| 478 | + "function first = |x| {\n" + | ||
| 479 | + " return helper(x)\n" + | ||
| 480 | + "}\n" + | ||
| 481 | + "\n" + | ||
| 482 | + "function second = |x| {\n" + | ||
| 483 | + " return helper(x) * 2\n" + | ||
| 484 | + "}\n" + | ||
| 485 | + "\n" + | ||
| 486 | + "\n" + | ||
| 487 | + "function main = |args| {\n" + | ||
| 488 | + " let text = \"hi\"\n" + | ||
| 489 | + " \n" + | ||
| 490 | + " println(first(1) + second(2) + text)\n" + | ||
| 491 | + "}\n" | ||
| 492 | + | ||
| 493 | +// brokenScript is a file that does not parse: the closing brace of main is | ||
| 494 | +// missing. It exists as a second fixture rather than as a line added to the | ||
| 495 | +// first, because a file holding a syntax error is a file whose *other* answers | ||
| 496 | +// are worth nothing: the completion test would then be measuring a parse that | ||
| 497 | +// never finished. | ||
| 498 | +const brokenScript = "module demo.Broken\n" + | ||
| 499 | + "\n" + | ||
| 500 | + "function main = |args| {\n" + | ||
| 501 | + " let x = (1 +\n" | ||
| 502 | + | ||
| 503 | +// Where the fixture's interesting lines are, counted from zero. | ||
| 504 | +const ( | ||
| 505 | + declarationLine = 14 | ||
| 506 | + completionLine = 18 | ||
| 507 | + helperLine = 3 | ||
| 508 | + helperColumn = 9 | ||
| 509 | + helperLineText = "function helper = |x| {" | ||
| 510 | + callLine = 8 | ||
| 511 | + callColumn = 9 | ||
| 512 | + callLineText = " return helper(x)" | ||
| 513 | + secondCallLine = 12 | ||
| 514 | +) | ||
| 515 | + | ||
| 516 | +// startRealServer writes a script, opens it, starts golo lsp and waits for it, | ||
| 517 | +// in the order the command does. It skips the test when golo is missing. | ||
| 518 | +func startRealServer(t *testing.T) (root string, editor *app.App) { | ||
| 519 | + t.Helper() | ||
| 520 | + return startRealServerOn(t, realScript) | ||
| 521 | +} | ||
| 522 | + | ||
| 523 | +// startRealServerOn is startRealServer over a chosen main.golo. | ||
| 524 | +func startRealServerOn(t *testing.T, source string) (root string, editor *app.App) { | ||
| 525 | + t.Helper() | ||
| 526 | + if testing.Short() { | ||
| 527 | + t.Skip("-short: not starting a language server") | ||
| 528 | + } | ||
| 529 | + | ||
| 530 | + server, err := lsp.FindServer(gololang.Profile().Server) | ||
| 531 | + if errors.Is(err, lsp.ErrServerNotFound) { | ||
| 532 | + t.Skipf("%s is not installed; %s", gololang.ServerCommand, gololang.InstallHint) | ||
| 533 | + } | ||
| 534 | + // Finding it is not the same as being able to run it: a shim left behind by | ||
| 535 | + // a tool manager whose environment has since been removed is on PATH and | ||
| 536 | + // fails only when started. | ||
| 537 | + if !serverRuns(server) { | ||
| 538 | + t.Skipf("%s at %s cannot run; %s", gololang.ServerCommand, server, gololang.InstallHint) | ||
| 539 | + } | ||
| 540 | + | ||
| 541 | + root = t.TempDir() | ||
| 542 | + writeFile(t, filepath.Join(root, "main.golo"), source) | ||
| 543 | + | ||
| 544 | + editor = newTestEditor(t) | ||
| 545 | + | ||
| 546 | + // 1. Open the file, exactly as main does — before there is any server. | ||
| 547 | + editor.Open(filepath.Join(root, "main.golo")) | ||
| 548 | + | ||
| 549 | + // 2. Start the language server, exactly as main does — afterwards, in the | ||
| 550 | + // file's own directory, which is what ProjectRoot answers with no | ||
| 551 | + // markers. | ||
| 552 | + ctx, cancel := context.WithCancel(t.Context()) | ||
| 553 | + t.Cleanup(cancel) | ||
| 554 | + editor.StartLanguageServer(ctx, root) | ||
| 555 | + t.Cleanup(func() { editor.Language().Stop(context.Background()) }) | ||
| 556 | + | ||
| 557 | + waitUntilReady(t, editor) | ||
| 558 | + | ||
| 559 | + // 3. Let the event loop notice the server is ready, as Run does on every | ||
| 560 | + // turn. This is what announces the file that was already open. | ||
| 561 | + editor.Tick() | ||
| 562 | + return root, editor | ||
| 563 | +} | ||
| 564 | + | ||
| 565 | +// newTestEditor returns Turbo Golo drawing on a simulated terminal, set up the | ||
| 566 | +// way the command sets it up. | ||
| 567 | +func newTestEditor(t *testing.T) *app.App { | ||
| 568 | + t.Helper() | ||
| 569 | + | ||
| 570 | + gololang.Register() | ||
| 571 | + screen := tcell.NewSimulationScreen("UTF-8") | ||
| 572 | + if err := screen.Init(); err != nil { | ||
| 573 | + t.Fatalf("initialising the simulation screen: %v", err) | ||
| 574 | + } | ||
| 575 | + t.Cleanup(screen.Fini) | ||
| 576 | + screen.SetSize(80, 24) | ||
| 577 | + | ||
| 578 | + // Never read the themes or snippets of whoever is running the tests. | ||
| 579 | + p := gololang.Profile() | ||
| 580 | + t.Setenv(p.ThemeDirEnvVar(), t.TempDir()) | ||
| 581 | + t.Setenv(p.SnippetDirEnvVar(), t.TempDir()) | ||
| 582 | + | ||
| 583 | + editor := app.New(screen, "turbo-classic", p) | ||
| 584 | + editor.Render() | ||
| 585 | + return editor | ||
| 586 | +} | ||
| 587 | + | ||
| 588 | +// typeText sends a run of printable characters through the whole routing chain. | ||
| 589 | +func typeText(editor *app.App, text string) { | ||
| 590 | + for _, r := range text { | ||
| 591 | + editor.Handle(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone)) | ||
| 592 | + } | ||
| 593 | +} | ||
| 594 | + | ||
| 595 | +// completionOffers reports whether the open popup holds an entry starting with | ||
| 596 | +// a label. | ||
| 597 | +func completionOffers(editor *app.App, label string) bool { | ||
| 598 | + for _, item := range editor.Completion().Matches() { | ||
| 599 | + if strings.HasPrefix(item.Label, label) { | ||
| 600 | + return true | ||
| 601 | + } | ||
| 602 | + } | ||
| 603 | + return false | ||
| 604 | +} | ||
| 605 | + | ||
| 606 | +// waitUntilReady blocks until the language server has finished starting. | ||
| 607 | +func waitUntilReady(t *testing.T, editor *app.App) { | ||
| 608 | + t.Helper() | ||
| 609 | + | ||
| 610 | + deadline := time.After(lsp.InitializeTimeout) | ||
| 611 | + for !editor.Language().Ready() { | ||
| 612 | + select { | ||
| 613 | + case <-deadline: | ||
| 614 | + t.Fatalf("the language server never became ready: %s", editor.Language().Status()) | ||
| 615 | + case <-time.After(10 * time.Millisecond): | ||
| 616 | + } | ||
| 617 | + } | ||
| 618 | +} | ||
| 619 | + | ||
| 620 | +// waitUntil polls a condition until it holds or the time runs out, and fails | ||
| 621 | +// the test if it never does. | ||
| 622 | +func waitUntil(t *testing.T, within time.Duration, done func() bool) { | ||
| 623 | + t.Helper() | ||
| 624 | + | ||
| 625 | + deadline := time.Now().Add(within) | ||
| 626 | + for time.Now().Before(deadline) { | ||
| 627 | + if done() { | ||
| 628 | + return | ||
| 629 | + } | ||
| 630 | + time.Sleep(200 * time.Millisecond) | ||
| 631 | + } | ||
| 632 | + t.Errorf("the server never answered within %s", within) | ||
| 633 | +} | ||
| 634 | + | ||
| 635 | +// waitForLocations asks a location question until it is answered, because a | ||
| 636 | +// server that is still indexing answers an empty list rather than an error. | ||
| 637 | +func waitForLocations(t *testing.T, ask func(context.Context) ([]lsp.Location, error)) []lsp.Location { | ||
| 638 | + t.Helper() | ||
| 639 | + | ||
| 640 | + var found []lsp.Location | ||
| 641 | + waitUntil(t, 30*time.Second, func() bool { | ||
| 642 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) | ||
| 643 | + defer cancel() | ||
| 644 | + | ||
| 645 | + locations, err := ask(ctx) | ||
| 646 | + if err != nil { | ||
| 647 | + return false | ||
| 648 | + } | ||
| 649 | + found = locations | ||
| 650 | + return len(found) > 0 | ||
| 651 | + }) | ||
| 652 | + return found | ||
| 653 | +} | ||
| 654 | + | ||
| 655 | +// waitForCompletion asks for a completion until one arrives, or gives up. | ||
| 656 | +// | ||
| 657 | +// A server may load its state after it has finished initialising, and answer | ||
| 658 | +// an empty list until that is done. There is no notification this client reads | ||
| 659 | +// that says when — so it asks again, which is what the editor's user would do. | ||
| 660 | +func waitForCompletion(t *testing.T, editor *app.App) bool { | ||
| 661 | + t.Helper() | ||
| 662 | + | ||
| 663 | + deadline := time.Now().Add(60 * time.Second) | ||
| 664 | + for time.Now().Before(deadline) { | ||
| 665 | + if editor.Completion().Visible() { | ||
| 666 | + return true | ||
| 667 | + } | ||
| 668 | + editor.RequestCompletion() | ||
| 669 | + if editor.Completion().Visible() { | ||
| 670 | + return true | ||
| 671 | + } | ||
| 672 | + time.Sleep(500 * time.Millisecond) | ||
| 673 | + } | ||
| 674 | + return false | ||
| 675 | +} | ||
| 676 | + | ||
| 677 | +// serverRuns reports whether the language server at path actually starts. | ||
| 678 | +func serverRuns(path string) bool { | ||
| 679 | + return exec.Command(path, "--version").Run() == nil | ||
| 680 | +} | ||
| 681 | + | ||
| 682 | +// writeFile creates a file, making its directory first. | ||
| 683 | +func writeFile(t *testing.T, path, content string) { | ||
| 684 | + t.Helper() | ||
| 685 | + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { | ||
| 686 | + t.Fatalf("creating %s: %v", filepath.Dir(path), err) | ||
| 687 | + } | ||
| 688 | + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { | ||
| 689 | + t.Fatalf("writing %s: %v", path, err) | ||
| 690 | + } | ||
| 691 | +} | ||
added
internal/gololang/gololang.go +143 -0 | new file mode 100644 | ||
| @@ -0,0 +1,143 @@ | ||
| 1 | +// Package gololang is everything about Turbo Golo that is about *Golo*: how | |
| 2 | +// the editor names itself, which language server it talks to, what a project's | |
| 3 | +// starter files say, and how Golo source is coloured. | |
| 4 | +// | |
| 5 | +// Everything else the editor does lives in turbo-core, which knows nothing | |
| 6 | +// about Golo. This package is the whole of the difference between Turbo Golo | |
| 7 | +// and Turbo MoonBit, which is what makes a sixth editor a matter of writing one | |
| 8 | +// of these rather than forking anything. | |
| 9 | +// | |
| 10 | +// gololang.Register() // teach the library to colour Golo | |
| 11 | +// editor := app.New(screen, name, gololang.Profile()) | |
| 12 | +package gololang | |
| 13 | + | |
| 14 | +import ( | |
| 15 | + "rickub.com/turbo-editors/turbo-core/profile" | |
| 16 | + "rickub.com/turbo-editors/turbo-core/syntax" | |
| 17 | +) | |
| 18 | + | |
| 19 | +// Name and Slug are what the editor calls itself. The slug is also its binary, | |
| 20 | +// its project directory (as .turbo-golo) and the stem of its environment | |
| 21 | +// variables (as TURBO_GOLO_…), so it is not free to change. | |
| 22 | +const ( | |
| 23 | + Name = "Turbo Golo" | |
| 24 | + Slug = "turbo-golo" | |
| 25 | +) | |
| 26 | + | |
| 27 | +// Language is the name Golo is known by: the value LanguageOf returns for a | |
| 28 | +// .golo file, and what a snippets file writes in its languages key. | |
| 29 | +// | |
| 30 | +// It is lower case because every other name in the registry is — "golo" | |
| 31 | +// beside "toml" and "dockerfile" — while the language's own spelling, the one | |
| 32 | +// a person reads, is Profile().Language. | |
| 33 | +const Language syntax.Language = "golo" | |
| 34 | + | |
| 35 | +// ServerCommand is the language server Turbo Golo talks to, and InstallHint | |
| 36 | +// the one place a user gets it from. | |
| 37 | +// | |
| 38 | +// The server is the interpreter itself: `golo lsp` puts the same binary that | |
| 39 | +// runs a script into language-server mode, reusing its lexer, parser and AST. | |
| 40 | +// So a machine that can run Golo can complete Golo, and there is nothing | |
| 41 | +// separate to install — which is why the hint names the release page that | |
| 42 | +// ships the binary rather than a package manager's command. | |
| 43 | +// | |
| 44 | +// It answers four of the nine questions turbo-core asks — completion, hover, | |
| 45 | +// definition and the file's symbols — and publishes diagnostics unasked. It | |
| 46 | +// advertises neither references, typeDefinition and implementation, nor | |
| 47 | +// workspace/symbol, so those items report nothing found. That is documented | |
| 48 | +// rather than worked around, and a test asserts it so a future golo gaining | |
| 49 | +// them is noticed. | |
| 50 | +const ( | |
| 51 | + ServerCommand = "golo" | |
| 52 | + InstallHint = "see https://codeberg.org/TypeUnsafe/golo-script/releases" | |
| 53 | +) | |
| 54 | + | |
| 55 | +// DefaultInstallDir is where GoloScript's own install.sh puts golo, gogolo and | |
| 56 | +// wagolo. It is on PATH on almost every machine, and it is searched all the | |
| 57 | +// same for the one where it is not: "completion silently does nothing" is what | |
| 58 | +// a user sees when the editor cannot find a server they believe they installed. | |
| 59 | +const DefaultInstallDir = "/usr/local/bin" | |
| 60 | + | |
| 61 | +// ServerArgs is what golo is started with. | |
| 62 | +// | |
| 63 | +// The lsp is not optional: golo with no argument starts its REPL and reads | |
| 64 | +// standard input as Golo source, which the editor would see as a server that | |
| 65 | +// answers nothing and never dies. It is a function rather than a variable so | |
| 66 | +// that no caller can append to the package's own slice. | |
| 67 | +func ServerArgs() []string { return []string{"lsp"} } | |
| 68 | + | |
| 69 | +// Profile returns the editor Turbo Golo is. | |
| 70 | +// | |
| 71 | +// It is a function rather than a variable for the same reason as in every other | |
| 72 | +// editor of this family, even though nothing here reads the environment: a | |
| 73 | +// caller that mutates what it gets back — appending to Server.Dirs, say — must | |
| 74 | +// not be mutating the package's one copy. | |
| 75 | +func Profile() profile.Profile { | |
| 76 | + return profile.Profile{ | |
| 77 | + Name: Name, | |
| 78 | + Slug: Slug, | |
| 79 | + // The language's own spelling. Golo is the language; GoloScript is | |
| 80 | + // the implementation whose binary this editor talks to, the way Go is | |
| 81 | + // the language and gc the compiler. This is what the About box and the | |
| 82 | + // status bar read out, so it names the language. | |
| 83 | + Language: "Golo", | |
| 84 | + // G is free: the fixed menus take F, E, S, R, C, O, W, N and H — | |
| 85 | + // which rules out the O of Golo but not its first letter — so the hot | |
| 86 | + // key lands where a reader expects it. The menu is named after the | |
| 87 | + // language and not after golo, gogolo or wagolo, because it holds | |
| 88 | + // whatever the project put in its tools file, and the first tools | |
| 89 | + // file anybody writes outgrows the language's own toolchain. | |
| 90 | + ToolsMenu: "~G~olo", | |
| 91 | + // None, on purpose. Golo has no project manifest — no golo.mod, no | |
| 92 | + // build file — a script is a file and a program is a directory of | |
| 93 | + // them. With nothing to look for, app.ProjectRoot hands the server | |
| 94 | + // the directory of the file being edited, which is also all the | |
| 95 | + // server needs: golo lsp answers about the file it is given and | |
| 96 | + // resolves imports from the modules embedded in the binary, never | |
| 97 | + // from disk. A marker would make the walk look like part of the rule | |
| 98 | + // when there is no rule. | |
| 99 | + RootMarkers: nil, | |
| 100 | + Server: profile.Server{ | |
| 101 | + Command: ServerCommand, | |
| 102 | + Args: ServerArgs(), | |
| 103 | + InstallHint: InstallHint, | |
| 104 | + Dirs: ServerDirs(), | |
| 105 | + }, | |
| 106 | + Templates: profile.Templates{ | |
| 107 | + Settings: settingsTemplate, | |
| 108 | + Snippets: snippetsTemplate, | |
| 109 | + Tools: toolsTemplate, | |
| 110 | + Agents: agentsTemplate, | |
| 111 | + }, | |
| 112 | + } | |
| 113 | +} | |
| 114 | + | |
| 115 | +// Register teaches turbo-core to colour Golo. | |
| 116 | +// | |
| 117 | +// It is called explicitly at start-up rather than from an init function so that | |
| 118 | +// "which languages does this editor know?" is answered by reading main, not by | |
| 119 | +// working out which packages were imported. | |
| 120 | +// | |
| 121 | +// A shebang is claimed. A Golo script may open with #!/usr/bin/env golo and be | |
| 122 | +// run as a command: # opens a comment in Golo, so the interpreter reads the | |
| 123 | +// line as one, and a file with no extension whose first line names golo is | |
| 124 | +// Golo and nothing else. | |
| 125 | +func Register() { | |
| 126 | + syntax.Register(syntax.Definition{ | |
| 127 | + Language: Language, | |
| 128 | + Extensions: []string{".golo"}, | |
| 129 | + Shebangs: []string{"golo"}, | |
| 130 | + Highlight: Highlight, | |
| 131 | + }) | |
| 132 | +} | |
| 133 | + | |
| 134 | +// ServerDirs returns the directories golo is looked for in after PATH. | |
| 135 | +// | |
| 136 | +// There is one: the directory GoloScript's installer writes into. Golo has no | |
| 137 | +// per-user toolchain directory and no environment variable naming one, so | |
| 138 | +// there is nothing else to search — and listing a guess would be worse than | |
| 139 | +// listing nothing, because a directory the server is never in costs a stat | |
| 140 | +// on every start for no answer. | |
| 141 | +func ServerDirs() []string { | |
| 142 | + return []string{DefaultInstallDir} | |
| 143 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,143 @@ | |||
| 1 | +// Package gololang is everything about Turbo Golo that is about *Golo*: how | ||
| 2 | +// the editor names itself, which language server it talks to, what a project's | ||
| 3 | +// starter files say, and how Golo source is coloured. | ||
| 4 | +// | ||
| 5 | +// Everything else the editor does lives in turbo-core, which knows nothing | ||
| 6 | +// about Golo. This package is the whole of the difference between Turbo Golo | ||
| 7 | +// and Turbo MoonBit, which is what makes a sixth editor a matter of writing one | ||
| 8 | +// of these rather than forking anything. | ||
| 9 | +// | ||
| 10 | +// gololang.Register() // teach the library to colour Golo | ||
| 11 | +// editor := app.New(screen, name, gololang.Profile()) | ||
| 12 | +package gololang | ||
| 13 | + | ||
| 14 | +import ( | ||
| 15 | + "rickub.com/turbo-editors/turbo-core/profile" | ||
| 16 | + "rickub.com/turbo-editors/turbo-core/syntax" | ||
| 17 | +) | ||
| 18 | + | ||
| 19 | +// Name and Slug are what the editor calls itself. The slug is also its binary, | ||
| 20 | +// its project directory (as .turbo-golo) and the stem of its environment | ||
| 21 | +// variables (as TURBO_GOLO_…), so it is not free to change. | ||
| 22 | +const ( | ||
| 23 | + Name = "Turbo Golo" | ||
| 24 | + Slug = "turbo-golo" | ||
| 25 | +) | ||
| 26 | + | ||
| 27 | +// Language is the name Golo is known by: the value LanguageOf returns for a | ||
| 28 | +// .golo file, and what a snippets file writes in its languages key. | ||
| 29 | +// | ||
| 30 | +// It is lower case because every other name in the registry is — "golo" | ||
| 31 | +// beside "toml" and "dockerfile" — while the language's own spelling, the one | ||
| 32 | +// a person reads, is Profile().Language. | ||
| 33 | +const Language syntax.Language = "golo" | ||
| 34 | + | ||
| 35 | +// ServerCommand is the language server Turbo Golo talks to, and InstallHint | ||
| 36 | +// the one place a user gets it from. | ||
| 37 | +// | ||
| 38 | +// The server is the interpreter itself: `golo lsp` puts the same binary that | ||
| 39 | +// runs a script into language-server mode, reusing its lexer, parser and AST. | ||
| 40 | +// So a machine that can run Golo can complete Golo, and there is nothing | ||
| 41 | +// separate to install — which is why the hint names the release page that | ||
| 42 | +// ships the binary rather than a package manager's command. | ||
| 43 | +// | ||
| 44 | +// It answers four of the nine questions turbo-core asks — completion, hover, | ||
| 45 | +// definition and the file's symbols — and publishes diagnostics unasked. It | ||
| 46 | +// advertises neither references, typeDefinition and implementation, nor | ||
| 47 | +// workspace/symbol, so those items report nothing found. That is documented | ||
| 48 | +// rather than worked around, and a test asserts it so a future golo gaining | ||
| 49 | +// them is noticed. | ||
| 50 | +const ( | ||
| 51 | + ServerCommand = "golo" | ||
| 52 | + InstallHint = "see https://codeberg.org/TypeUnsafe/golo-script/releases" | ||
| 53 | +) | ||
| 54 | + | ||
| 55 | +// DefaultInstallDir is where GoloScript's own install.sh puts golo, gogolo and | ||
| 56 | +// wagolo. It is on PATH on almost every machine, and it is searched all the | ||
| 57 | +// same for the one where it is not: "completion silently does nothing" is what | ||
| 58 | +// a user sees when the editor cannot find a server they believe they installed. | ||
| 59 | +const DefaultInstallDir = "/usr/local/bin" | ||
| 60 | + | ||
| 61 | +// ServerArgs is what golo is started with. | ||
| 62 | +// | ||
| 63 | +// The lsp is not optional: golo with no argument starts its REPL and reads | ||
| 64 | +// standard input as Golo source, which the editor would see as a server that | ||
| 65 | +// answers nothing and never dies. It is a function rather than a variable so | ||
| 66 | +// that no caller can append to the package's own slice. | ||
| 67 | +func ServerArgs() []string { return []string{"lsp"} } | ||
| 68 | + | ||
| 69 | +// Profile returns the editor Turbo Golo is. | ||
| 70 | +// | ||
| 71 | +// It is a function rather than a variable for the same reason as in every other | ||
| 72 | +// editor of this family, even though nothing here reads the environment: a | ||
| 73 | +// caller that mutates what it gets back — appending to Server.Dirs, say — must | ||
| 74 | +// not be mutating the package's one copy. | ||
| 75 | +func Profile() profile.Profile { | ||
| 76 | + return profile.Profile{ | ||
| 77 | + Name: Name, | ||
| 78 | + Slug: Slug, | ||
| 79 | + // The language's own spelling. Golo is the language; GoloScript is | ||
| 80 | + // the implementation whose binary this editor talks to, the way Go is | ||
| 81 | + // the language and gc the compiler. This is what the About box and the | ||
| 82 | + // status bar read out, so it names the language. | ||
| 83 | + Language: "Golo", | ||
| 84 | + // G is free: the fixed menus take F, E, S, R, C, O, W, N and H — | ||
| 85 | + // which rules out the O of Golo but not its first letter — so the hot | ||
| 86 | + // key lands where a reader expects it. The menu is named after the | ||
| 87 | + // language and not after golo, gogolo or wagolo, because it holds | ||
| 88 | + // whatever the project put in its tools file, and the first tools | ||
| 89 | + // file anybody writes outgrows the language's own toolchain. | ||
| 90 | + ToolsMenu: "~G~olo", | ||
| 91 | + // None, on purpose. Golo has no project manifest — no golo.mod, no | ||
| 92 | + // build file — a script is a file and a program is a directory of | ||
| 93 | + // them. With nothing to look for, app.ProjectRoot hands the server | ||
| 94 | + // the directory of the file being edited, which is also all the | ||
| 95 | + // server needs: golo lsp answers about the file it is given and | ||
| 96 | + // resolves imports from the modules embedded in the binary, never | ||
| 97 | + // from disk. A marker would make the walk look like part of the rule | ||
| 98 | + // when there is no rule. | ||
| 99 | + RootMarkers: nil, | ||
| 100 | + Server: profile.Server{ | ||
| 101 | + Command: ServerCommand, | ||
| 102 | + Args: ServerArgs(), | ||
| 103 | + InstallHint: InstallHint, | ||
| 104 | + Dirs: ServerDirs(), | ||
| 105 | + }, | ||
| 106 | + Templates: profile.Templates{ | ||
| 107 | + Settings: settingsTemplate, | ||
| 108 | + Snippets: snippetsTemplate, | ||
| 109 | + Tools: toolsTemplate, | ||
| 110 | + Agents: agentsTemplate, | ||
| 111 | + }, | ||
| 112 | + } | ||
| 113 | +} | ||
| 114 | + | ||
| 115 | +// Register teaches turbo-core to colour Golo. | ||
| 116 | +// | ||
| 117 | +// It is called explicitly at start-up rather than from an init function so that | ||
| 118 | +// "which languages does this editor know?" is answered by reading main, not by | ||
| 119 | +// working out which packages were imported. | ||
| 120 | +// | ||
| 121 | +// A shebang is claimed. A Golo script may open with #!/usr/bin/env golo and be | ||
| 122 | +// run as a command: # opens a comment in Golo, so the interpreter reads the | ||
| 123 | +// line as one, and a file with no extension whose first line names golo is | ||
| 124 | +// Golo and nothing else. | ||
| 125 | +func Register() { | ||
| 126 | + syntax.Register(syntax.Definition{ | ||
| 127 | + Language: Language, | ||
| 128 | + Extensions: []string{".golo"}, | ||
| 129 | + Shebangs: []string{"golo"}, | ||
| 130 | + Highlight: Highlight, | ||
| 131 | + }) | ||
| 132 | +} | ||
| 133 | + | ||
| 134 | +// ServerDirs returns the directories golo is looked for in after PATH. | ||
| 135 | +// | ||
| 136 | +// There is one: the directory GoloScript's installer writes into. Golo has no | ||
| 137 | +// per-user toolchain directory and no environment variable naming one, so | ||
| 138 | +// there is nothing else to search — and listing a guess would be worse than | ||
| 139 | +// listing nothing, because a directory the server is never in costs a stat | ||
| 140 | +// on every start for no answer. | ||
| 141 | +func ServerDirs() []string { | ||
| 142 | + return []string{DefaultInstallDir} | ||
| 143 | +} | ||
added
internal/gololang/literals.go +80 -0 | new file mode 100644 | ||
| @@ -0,0 +1,80 @@ | ||
| 1 | +package gololang | |
| 2 | + | |
| 3 | +// The quoted literals of Golo: "strings", """multi-line strings""" and | |
| 4 | +// 'characters', and the one rule that governs all three — each runs to its | |
| 5 | +// closing quote, wherever that quote is. | |
| 6 | + | |
| 7 | +import "rickub.com/turbo-editors/turbo-core/syntax" | |
| 8 | + | |
| 9 | +// multilineStringQuote opens and closes a multi-line string. Inside one the | |
| 10 | +// lexer interprets nothing — no escape, no interpolation — so the only thing | |
| 11 | +// that can end it is these three runes. | |
| 12 | +const multilineStringQuote = `"""` | |
| 13 | + | |
| 14 | +// takeQuoted colours a "…" or '…' literal from its opening quote to its | |
| 15 | +// closing one, and reports it as still open when the line ends first. | |
| 16 | +// | |
| 17 | +// Carrying it is the honest reading of the language. The interpreter reads a | |
| 18 | +// string to its closing quote and nothing stops it at a newline, so a line | |
| 19 | +// ending inside a literal is not broken source — it is a literal with a line | |
| 20 | +// break in it — and stopping the colour at the line would draw the next line | |
| 21 | +// as code the interpreter will never run as code. | |
| 22 | +// | |
| 23 | +// A character literal is read the same way, by the same loop in lexer.go, so | |
| 24 | +// it is carried the same way. That a 'character' spanning lines is nonsense to | |
| 25 | +// a reader is beside the point: the colour says what the interpreter will do. | |
| 26 | +func takeQuoted(s *syntax.LineScanner, quote rune, class syntax.Class, ifOpen carry) carry { | |
| 27 | + start := s.Pos() | |
| 28 | + s.Advance(1) // the opening quote | |
| 29 | + | |
| 30 | + closed := consumeQuoted(s, quote) | |
| 31 | + s.Emit(start, s.Pos(), class) | |
| 32 | + return stillOpen(closed, ifOpen) | |
| 33 | +} | |
| 34 | + | |
| 35 | +// finishQuoted colours the rest of a literal opened on an earlier line, and | |
| 36 | +// says whether it is still open at the end of this one. | |
| 37 | +func finishQuoted(s *syntax.LineScanner, quote rune, class syntax.Class, ifOpen carry) carry { | |
| 38 | + closed := consumeQuoted(s, quote) | |
| 39 | + s.Emit(0, s.Pos(), class) | |
| 40 | + return stillOpen(closed, ifOpen) | |
| 41 | +} | |
| 42 | + | |
| 43 | +// consumeQuoted runs to the closing quote, or to the end of the line, and | |
| 44 | +// reports which it found. | |
| 45 | +// | |
| 46 | +// A backslash takes the rune after it out of consideration. That one rule | |
| 47 | +// covers every escape the lexer knows — \n, \t, \r, \\, \", \', \0 and \xHH — | |
| 48 | +// and the ones it does not, which it keeps as the character itself; all any of | |
| 49 | +// them need from this scanner is that the rune after the backslash cannot | |
| 50 | +// close the literal. A backslash that ends the line escapes the newline, which | |
| 51 | +// the lexer also reads as part of the string, and the carry covers that. | |
| 52 | +func consumeQuoted(s *syntax.LineScanner, quote rune) bool { | |
| 53 | + for !s.AtEnd() { | |
| 54 | + switch s.Peek(0) { | |
| 55 | + case '\\': | |
| 56 | + s.Advance(2) | |
| 57 | + case quote: | |
| 58 | + s.Advance(1) | |
| 59 | + return true | |
| 60 | + default: | |
| 61 | + s.Advance(1) | |
| 62 | + } | |
| 63 | + } | |
| 64 | + return false | |
| 65 | +} | |
| 66 | + | |
| 67 | +// takeMultilineString colours a """…""" literal from its opening quotes to its | |
| 68 | +// closing ones, and reports it as still open when the line ends first. | |
| 69 | +// | |
| 70 | +// syntax.OpenBlockComment is named for comments, and what it does is exactly | |
| 71 | +// this: from an opener to the first closer, or to the end of the line, in one | |
| 72 | +// class, with no escapes considered. A multi-line string in Golo has no | |
| 73 | +// escapes either — the lexer appends every rune it meets until the three | |
| 74 | +// quotes — so the helper is the right tool under a misleading name, and the | |
| 75 | +// continuation on later lines goes through FinishBlockComment for the same | |
| 76 | +// reason. | |
| 77 | +func takeMultilineString(s *syntax.LineScanner) carry { | |
| 78 | + closed := syntax.OpenBlockComment(s, multilineStringQuote, multilineStringQuote, syntax.ClassString) | |
| 79 | + return stillOpen(closed, multilineStringOpen) | |
| 80 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,80 @@ | |||
| 1 | +package gololang | ||
| 2 | + | ||
| 3 | +// The quoted literals of Golo: "strings", """multi-line strings""" and | ||
| 4 | +// 'characters', and the one rule that governs all three — each runs to its | ||
| 5 | +// closing quote, wherever that quote is. | ||
| 6 | + | ||
| 7 | +import "rickub.com/turbo-editors/turbo-core/syntax" | ||
| 8 | + | ||
| 9 | +// multilineStringQuote opens and closes a multi-line string. Inside one the | ||
| 10 | +// lexer interprets nothing — no escape, no interpolation — so the only thing | ||
| 11 | +// that can end it is these three runes. | ||
| 12 | +const multilineStringQuote = `"""` | ||
| 13 | + | ||
| 14 | +// takeQuoted colours a "…" or '…' literal from its opening quote to its | ||
| 15 | +// closing one, and reports it as still open when the line ends first. | ||
| 16 | +// | ||
| 17 | +// Carrying it is the honest reading of the language. The interpreter reads a | ||
| 18 | +// string to its closing quote and nothing stops it at a newline, so a line | ||
| 19 | +// ending inside a literal is not broken source — it is a literal with a line | ||
| 20 | +// break in it — and stopping the colour at the line would draw the next line | ||
| 21 | +// as code the interpreter will never run as code. | ||
| 22 | +// | ||
| 23 | +// A character literal is read the same way, by the same loop in lexer.go, so | ||
| 24 | +// it is carried the same way. That a 'character' spanning lines is nonsense to | ||
| 25 | +// a reader is beside the point: the colour says what the interpreter will do. | ||
| 26 | +func takeQuoted(s *syntax.LineScanner, quote rune, class syntax.Class, ifOpen carry) carry { | ||
| 27 | + start := s.Pos() | ||
| 28 | + s.Advance(1) // the opening quote | ||
| 29 | + | ||
| 30 | + closed := consumeQuoted(s, quote) | ||
| 31 | + s.Emit(start, s.Pos(), class) | ||
| 32 | + return stillOpen(closed, ifOpen) | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +// finishQuoted colours the rest of a literal opened on an earlier line, and | ||
| 36 | +// says whether it is still open at the end of this one. | ||
| 37 | +func finishQuoted(s *syntax.LineScanner, quote rune, class syntax.Class, ifOpen carry) carry { | ||
| 38 | + closed := consumeQuoted(s, quote) | ||
| 39 | + s.Emit(0, s.Pos(), class) | ||
| 40 | + return stillOpen(closed, ifOpen) | ||
| 41 | +} | ||
| 42 | + | ||
| 43 | +// consumeQuoted runs to the closing quote, or to the end of the line, and | ||
| 44 | +// reports which it found. | ||
| 45 | +// | ||
| 46 | +// A backslash takes the rune after it out of consideration. That one rule | ||
| 47 | +// covers every escape the lexer knows — \n, \t, \r, \\, \", \', \0 and \xHH — | ||
| 48 | +// and the ones it does not, which it keeps as the character itself; all any of | ||
| 49 | +// them need from this scanner is that the rune after the backslash cannot | ||
| 50 | +// close the literal. A backslash that ends the line escapes the newline, which | ||
| 51 | +// the lexer also reads as part of the string, and the carry covers that. | ||
| 52 | +func consumeQuoted(s *syntax.LineScanner, quote rune) bool { | ||
| 53 | + for !s.AtEnd() { | ||
| 54 | + switch s.Peek(0) { | ||
| 55 | + case '\\': | ||
| 56 | + s.Advance(2) | ||
| 57 | + case quote: | ||
| 58 | + s.Advance(1) | ||
| 59 | + return true | ||
| 60 | + default: | ||
| 61 | + s.Advance(1) | ||
| 62 | + } | ||
| 63 | + } | ||
| 64 | + return false | ||
| 65 | +} | ||
| 66 | + | ||
| 67 | +// takeMultilineString colours a """…""" literal from its opening quotes to its | ||
| 68 | +// closing ones, and reports it as still open when the line ends first. | ||
| 69 | +// | ||
| 70 | +// syntax.OpenBlockComment is named for comments, and what it does is exactly | ||
| 71 | +// this: from an opener to the first closer, or to the end of the line, in one | ||
| 72 | +// class, with no escapes considered. A multi-line string in Golo has no | ||
| 73 | +// escapes either — the lexer appends every rune it meets until the three | ||
| 74 | +// quotes — so the helper is the right tool under a misleading name, and the | ||
| 75 | +// continuation on later lines goes through FinishBlockComment for the same | ||
| 76 | +// reason. | ||
| 77 | +func takeMultilineString(s *syntax.LineScanner) carry { | ||
| 78 | + closed := syntax.OpenBlockComment(s, multilineStringQuote, multilineStringQuote, syntax.ClassString) | ||
| 79 | + return stillOpen(closed, multilineStringOpen) | ||
| 80 | +} | ||
added
internal/gololang/profile_test.go +226 -0 | new file mode 100644 | ||
| @@ -0,0 +1,226 @@ | ||
| 1 | +package gololang_test | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "slices" | |
| 5 | + "strings" | |
| 6 | + "testing" | |
| 7 | + | |
| 8 | + "rickub.com/turbo-editors/turbo-core/profile" | |
| 9 | + "rickub.com/turbo-editors/turbo-core/syntax" | |
| 10 | + | |
| 11 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | |
| 12 | +) | |
| 13 | + | |
| 14 | +// fixedMenuHotKeys are the hot keys turbo-core's own menus take. The toolchain | |
| 15 | +// menu may not claim one of them, or one of the two would be unreachable from | |
| 16 | +// the keyboard and nothing would say so. | |
| 17 | +var fixedMenuHotKeys = []rune{'F', 'E', 'S', 'R', 'C', 'O', 'W', 'N', 'H'} | |
| 18 | + | |
| 19 | +func TestProfileNamesTheEditor(t *testing.T) { | |
| 20 | + p := gololang.Profile() | |
| 21 | + | |
| 22 | + if p.Name != "Turbo Golo" { | |
| 23 | + t.Errorf("Name = %q, want %q", p.Name, "Turbo Golo") | |
| 24 | + } | |
| 25 | + if p.Slug != "turbo-golo" { | |
| 26 | + t.Errorf("Slug = %q, want %q", p.Slug, "turbo-golo") | |
| 27 | + } | |
| 28 | + // The About box and the status bar read this out. It names the language, | |
| 29 | + // not the implementation: Golo is what the files are written in, and | |
| 30 | + // GoloScript is the binary the editor talks to. | |
| 31 | + if p.Language != "Golo" { | |
| 32 | + t.Errorf("Language = %q, want %q", p.Language, "Golo") | |
| 33 | + } | |
| 34 | +} | |
| 35 | + | |
| 36 | +func TestSlugDerivesEveryPath(t *testing.T) { | |
| 37 | + p := gololang.Profile() | |
| 38 | + | |
| 39 | + if got := p.ProjectDir(); got != ".turbo-golo" { | |
| 40 | + t.Errorf("ProjectDir() = %q, want %q", got, ".turbo-golo") | |
| 41 | + } | |
| 42 | + for name, got := range map[string]string{ | |
| 43 | + "DirEnvVar": p.DirEnvVar(), | |
| 44 | + "ThemeDirEnvVar": p.ThemeDirEnvVar(), | |
| 45 | + "SnippetDirEnvVar": p.SnippetDirEnvVar(), | |
| 46 | + } { | |
| 47 | + if !strings.HasPrefix(got, "TURBO_GOLO_") { | |
| 48 | + t.Errorf("%s() = %q, want a TURBO_GOLO_ prefix", name, got) | |
| 49 | + } | |
| 50 | + } | |
| 51 | +} | |
| 52 | + | |
| 53 | +func TestThemeDirFollowsItsEnvironmentVariable(t *testing.T) { | |
| 54 | + p := gololang.Profile() | |
| 55 | + want := t.TempDir() | |
| 56 | + t.Setenv(p.ThemeDirEnvVar(), want) | |
| 57 | + | |
| 58 | + if got := p.ThemeDir(); got != want { | |
| 59 | + t.Errorf("ThemeDir() = %q, want %q", got, want) | |
| 60 | + } | |
| 61 | +} | |
| 62 | + | |
| 63 | +func TestToolsMenuHotKeyClashesWithNoFixedMenu(t *testing.T) { | |
| 64 | + label := gololang.Profile().ToolsMenu | |
| 65 | + | |
| 66 | + hotKey, ok := hotKeyOf(label) | |
| 67 | + if !ok { | |
| 68 | + t.Fatalf("ToolsMenu = %q, which marks no hot key between tildes", label) | |
| 69 | + } | |
| 70 | + if slices.Contains(fixedMenuHotKeys, hotKey) { | |
| 71 | + t.Errorf("ToolsMenu hot key %q is already taken by a fixed menu", hotKey) | |
| 72 | + } | |
| 73 | + if got := strings.ReplaceAll(label, "~", ""); got != "Golo" { | |
| 74 | + t.Errorf("ToolsMenu reads %q once the tildes are removed, want %q", got, "Golo") | |
| 75 | + } | |
| 76 | +} | |
| 77 | + | |
| 78 | +// hotKeyOf returns the upper-case letter a menu label marks between tildes. | |
| 79 | +func hotKeyOf(label string) (rune, bool) { | |
| 80 | + open := strings.Index(label, "~") | |
| 81 | + if open < 0 { | |
| 82 | + return 0, false | |
| 83 | + } | |
| 84 | + rest := label[open+1:] | |
| 85 | + shut := strings.Index(rest, "~") | |
| 86 | + if shut != 1 { | |
| 87 | + return 0, false | |
| 88 | + } | |
| 89 | + return []rune(strings.ToUpper(rest))[0], true | |
| 90 | +} | |
| 91 | + | |
| 92 | +func TestThereAreNoRootMarkers(t *testing.T) { | |
| 93 | + // Golo has no project manifest, so there is nothing to walk up towards. | |
| 94 | + // With no markers app.ProjectRoot hands the server the directory of the | |
| 95 | + // file being edited, which is what main_test.go checks from the other | |
| 96 | + // side. A marker appearing here would be a claim about the language that | |
| 97 | + // the language does not make. | |
| 98 | + if got := gololang.Profile().RootMarkers; len(got) != 0 { | |
| 99 | + t.Errorf("RootMarkers = %v, want none", got) | |
| 100 | + } | |
| 101 | +} | |
| 102 | + | |
| 103 | +func TestServerIsTheInterpreterInLSPMode(t *testing.T) { | |
| 104 | + server := gololang.Profile().Server | |
| 105 | + | |
| 106 | + if server.Command != "golo" { | |
| 107 | + t.Errorf("Server.Command = %q, want %q", server.Command, "golo") | |
| 108 | + } | |
| 109 | + // golo with no argument starts a REPL that reads standard input as Golo, | |
| 110 | + // which the editor would see as a server that never answers. | |
| 111 | + if !slices.Equal(server.Args, []string{"lsp"}) { | |
| 112 | + t.Errorf("Server.Args = %v, want exactly [lsp]", server.Args) | |
| 113 | + } | |
| 114 | + if server.InstallHint == "" { | |
| 115 | + t.Error("Server.InstallHint is empty; a missing server would say nothing useful") | |
| 116 | + } | |
| 117 | + // It is shown on the status bar, so it has to fit on a narrow line. | |
| 118 | + if len(server.InstallHint) > 72 { | |
| 119 | + t.Errorf("InstallHint is %d characters, too long for a status bar", len(server.InstallHint)) | |
| 120 | + } | |
| 121 | +} | |
| 122 | + | |
| 123 | +func TestServerArgsCannotBeAppendedToByACaller(t *testing.T) { | |
| 124 | + first := gololang.ServerArgs() | |
| 125 | + first = append(first, "--nonsense") | |
| 126 | + | |
| 127 | + if second := gololang.ServerArgs(); slices.Contains(second, "--nonsense") { | |
| 128 | + t.Errorf("ServerArgs() = %v after a caller appended to an earlier result", second) | |
| 129 | + } | |
| 130 | +} | |
| 131 | + | |
| 132 | +func TestServerIsLookedForWhereGoloScriptInstallsIt(t *testing.T) { | |
| 133 | + // GoloScript's install.sh copies golo, gogolo and wagolo into | |
| 134 | + // /usr/local/bin. It is on PATH on nearly every machine, and the editor | |
| 135 | + // searches it anyway for the one where it is not. | |
| 136 | + dirs := gololang.Profile().Server.Dirs | |
| 137 | + | |
| 138 | + if !slices.Contains(dirs, "/usr/local/bin") { | |
| 139 | + t.Errorf("Server.Dirs = %v, want it to contain /usr/local/bin", dirs) | |
| 140 | + } | |
| 141 | + if len(dirs) != 1 { | |
| 142 | + t.Errorf("Server.Dirs = %v, want the one directory the installer writes into and no guesses", dirs) | |
| 143 | + } | |
| 144 | +} | |
| 145 | + | |
| 146 | +func TestProfileHandsOutAFreshCopyEveryTime(t *testing.T) { | |
| 147 | + // A caller that appends to Server.Dirs must not be appending to the | |
| 148 | + // package's one copy, or the next caller would inherit its guess. | |
| 149 | + first := gololang.Profile() | |
| 150 | + first.Server.Dirs = append(first.Server.Dirs, "/somewhere/else") | |
| 151 | + | |
| 152 | + if second := gololang.Profile(); slices.Contains(second.Server.Dirs, "/somewhere/else") { | |
| 153 | + t.Errorf("Profile().Server.Dirs = %v after a caller appended to an earlier result", second.Server.Dirs) | |
| 154 | + } | |
| 155 | +} | |
| 156 | + | |
| 157 | +func TestTemplatesAreAllFilledIn(t *testing.T) { | |
| 158 | + templates := gololang.Profile().Templates | |
| 159 | + | |
| 160 | + for name, template := range map[string]string{ | |
| 161 | + "Settings": templates.Settings, | |
| 162 | + "Snippets": templates.Snippets, | |
| 163 | + "Tools": templates.Tools, | |
| 164 | + } { | |
| 165 | + if template == "" { | |
| 166 | + t.Errorf("Templates.%s is empty; the editor would offer to write nothing", name) | |
| 167 | + } | |
| 168 | + } | |
| 169 | +} | |
| 170 | + | |
| 171 | +func TestRegisterTeachesTheLibraryGolo(t *testing.T) { | |
| 172 | + gololang.Register() | |
| 173 | + | |
| 174 | + if !slices.Contains(syntax.Registered(), gololang.Language) { | |
| 175 | + t.Fatalf("Registered() = %v, want it to contain %q", syntax.Registered(), gololang.Language) | |
| 176 | + } | |
| 177 | + | |
| 178 | + for _, path := range []string{"main.golo", "lib/strings_test.golo", "DEEP/nested/x.GOLO"} { | |
| 179 | + if got := syntax.LanguageOf(path, ""); got != gololang.Language { | |
| 180 | + t.Errorf("LanguageOf(%q) = %q, want %q", path, got, gololang.Language) | |
| 181 | + } | |
| 182 | + } | |
| 183 | +} | |
| 184 | + | |
| 185 | +func TestAShebangNamingGoloIsGolo(t *testing.T) { | |
| 186 | + // A script run as a command has no extension and opens with a line the | |
| 187 | + // interpreter reads as a comment. The first line is what identifies it. | |
| 188 | + gololang.Register() | |
| 189 | + | |
| 190 | + for _, firstLine := range []string{"#!/usr/bin/env golo", "#!/usr/local/bin/golo"} { | |
| 191 | + if got := syntax.LanguageOf("run-me", firstLine); got != gololang.Language { | |
| 192 | + t.Errorf("LanguageOf(%q) = %q, want %q", firstLine, got, gololang.Language) | |
| 193 | + } | |
| 194 | + } | |
| 195 | +} | |
| 196 | + | |
| 197 | +func TestRegisterLeavesOtherFilesAlone(t *testing.T) { | |
| 198 | + gololang.Register() | |
| 199 | + | |
| 200 | + cases := map[string]syntax.Language{ | |
| 201 | + "README.md": syntax.LanguageMarkdown, | |
| 202 | + "Dockerfile": syntax.LanguageDockerfile, | |
| 203 | + "main.py": syntax.LanguageNone, | |
| 204 | + "main.mbt": syntax.LanguageNone, | |
| 205 | + // A golo file's neighbour with a different extension is not Golo | |
| 206 | + // however Golo-flavoured its name. | |
| 207 | + "golo.toml": syntax.LanguageTOML, | |
| 208 | + } | |
| 209 | + for path, want := range cases { | |
| 210 | + if got := syntax.LanguageOf(path, ""); got != want { | |
| 211 | + t.Errorf("LanguageOf(%q) = %q, want %q", path, got, want) | |
| 212 | + } | |
| 213 | + } | |
| 214 | + // A shebang naming another interpreter is not Golo either. | |
| 215 | + if got := syntax.LanguageOf("script", "#!/usr/bin/env python3"); got == gololang.Language { | |
| 216 | + t.Errorf("LanguageOf with a python shebang = %q, want anything but Golo", got) | |
| 217 | + } | |
| 218 | +} | |
| 219 | + | |
| 220 | +// A profile is a literal, so this example is the whole of how one is used. | |
| 221 | +func ExampleProfile() { | |
| 222 | + gololang.Register() | |
| 223 | + | |
| 224 | + var p profile.Profile = gololang.Profile() | |
| 225 | + _ = p.ProjectDir() // ".turbo-golo" | |
| 226 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,226 @@ | |||
| 1 | +package gololang_test | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "slices" | ||
| 5 | + "strings" | ||
| 6 | + "testing" | ||
| 7 | + | ||
| 8 | + "rickub.com/turbo-editors/turbo-core/profile" | ||
| 9 | + "rickub.com/turbo-editors/turbo-core/syntax" | ||
| 10 | + | ||
| 11 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | ||
| 12 | +) | ||
| 13 | + | ||
| 14 | +// fixedMenuHotKeys are the hot keys turbo-core's own menus take. The toolchain | ||
| 15 | +// menu may not claim one of them, or one of the two would be unreachable from | ||
| 16 | +// the keyboard and nothing would say so. | ||
| 17 | +var fixedMenuHotKeys = []rune{'F', 'E', 'S', 'R', 'C', 'O', 'W', 'N', 'H'} | ||
| 18 | + | ||
| 19 | +func TestProfileNamesTheEditor(t *testing.T) { | ||
| 20 | + p := gololang.Profile() | ||
| 21 | + | ||
| 22 | + if p.Name != "Turbo Golo" { | ||
| 23 | + t.Errorf("Name = %q, want %q", p.Name, "Turbo Golo") | ||
| 24 | + } | ||
| 25 | + if p.Slug != "turbo-golo" { | ||
| 26 | + t.Errorf("Slug = %q, want %q", p.Slug, "turbo-golo") | ||
| 27 | + } | ||
| 28 | + // The About box and the status bar read this out. It names the language, | ||
| 29 | + // not the implementation: Golo is what the files are written in, and | ||
| 30 | + // GoloScript is the binary the editor talks to. | ||
| 31 | + if p.Language != "Golo" { | ||
| 32 | + t.Errorf("Language = %q, want %q", p.Language, "Golo") | ||
| 33 | + } | ||
| 34 | +} | ||
| 35 | + | ||
| 36 | +func TestSlugDerivesEveryPath(t *testing.T) { | ||
| 37 | + p := gololang.Profile() | ||
| 38 | + | ||
| 39 | + if got := p.ProjectDir(); got != ".turbo-golo" { | ||
| 40 | + t.Errorf("ProjectDir() = %q, want %q", got, ".turbo-golo") | ||
| 41 | + } | ||
| 42 | + for name, got := range map[string]string{ | ||
| 43 | + "DirEnvVar": p.DirEnvVar(), | ||
| 44 | + "ThemeDirEnvVar": p.ThemeDirEnvVar(), | ||
| 45 | + "SnippetDirEnvVar": p.SnippetDirEnvVar(), | ||
| 46 | + } { | ||
| 47 | + if !strings.HasPrefix(got, "TURBO_GOLO_") { | ||
| 48 | + t.Errorf("%s() = %q, want a TURBO_GOLO_ prefix", name, got) | ||
| 49 | + } | ||
| 50 | + } | ||
| 51 | +} | ||
| 52 | + | ||
| 53 | +func TestThemeDirFollowsItsEnvironmentVariable(t *testing.T) { | ||
| 54 | + p := gololang.Profile() | ||
| 55 | + want := t.TempDir() | ||
| 56 | + t.Setenv(p.ThemeDirEnvVar(), want) | ||
| 57 | + | ||
| 58 | + if got := p.ThemeDir(); got != want { | ||
| 59 | + t.Errorf("ThemeDir() = %q, want %q", got, want) | ||
| 60 | + } | ||
| 61 | +} | ||
| 62 | + | ||
| 63 | +func TestToolsMenuHotKeyClashesWithNoFixedMenu(t *testing.T) { | ||
| 64 | + label := gololang.Profile().ToolsMenu | ||
| 65 | + | ||
| 66 | + hotKey, ok := hotKeyOf(label) | ||
| 67 | + if !ok { | ||
| 68 | + t.Fatalf("ToolsMenu = %q, which marks no hot key between tildes", label) | ||
| 69 | + } | ||
| 70 | + if slices.Contains(fixedMenuHotKeys, hotKey) { | ||
| 71 | + t.Errorf("ToolsMenu hot key %q is already taken by a fixed menu", hotKey) | ||
| 72 | + } | ||
| 73 | + if got := strings.ReplaceAll(label, "~", ""); got != "Golo" { | ||
| 74 | + t.Errorf("ToolsMenu reads %q once the tildes are removed, want %q", got, "Golo") | ||
| 75 | + } | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +// hotKeyOf returns the upper-case letter a menu label marks between tildes. | ||
| 79 | +func hotKeyOf(label string) (rune, bool) { | ||
| 80 | + open := strings.Index(label, "~") | ||
| 81 | + if open < 0 { | ||
| 82 | + return 0, false | ||
| 83 | + } | ||
| 84 | + rest := label[open+1:] | ||
| 85 | + shut := strings.Index(rest, "~") | ||
| 86 | + if shut != 1 { | ||
| 87 | + return 0, false | ||
| 88 | + } | ||
| 89 | + return []rune(strings.ToUpper(rest))[0], true | ||
| 90 | +} | ||
| 91 | + | ||
| 92 | +func TestThereAreNoRootMarkers(t *testing.T) { | ||
| 93 | + // Golo has no project manifest, so there is nothing to walk up towards. | ||
| 94 | + // With no markers app.ProjectRoot hands the server the directory of the | ||
| 95 | + // file being edited, which is what main_test.go checks from the other | ||
| 96 | + // side. A marker appearing here would be a claim about the language that | ||
| 97 | + // the language does not make. | ||
| 98 | + if got := gololang.Profile().RootMarkers; len(got) != 0 { | ||
| 99 | + t.Errorf("RootMarkers = %v, want none", got) | ||
| 100 | + } | ||
| 101 | +} | ||
| 102 | + | ||
| 103 | +func TestServerIsTheInterpreterInLSPMode(t *testing.T) { | ||
| 104 | + server := gololang.Profile().Server | ||
| 105 | + | ||
| 106 | + if server.Command != "golo" { | ||
| 107 | + t.Errorf("Server.Command = %q, want %q", server.Command, "golo") | ||
| 108 | + } | ||
| 109 | + // golo with no argument starts a REPL that reads standard input as Golo, | ||
| 110 | + // which the editor would see as a server that never answers. | ||
| 111 | + if !slices.Equal(server.Args, []string{"lsp"}) { | ||
| 112 | + t.Errorf("Server.Args = %v, want exactly [lsp]", server.Args) | ||
| 113 | + } | ||
| 114 | + if server.InstallHint == "" { | ||
| 115 | + t.Error("Server.InstallHint is empty; a missing server would say nothing useful") | ||
| 116 | + } | ||
| 117 | + // It is shown on the status bar, so it has to fit on a narrow line. | ||
| 118 | + if len(server.InstallHint) > 72 { | ||
| 119 | + t.Errorf("InstallHint is %d characters, too long for a status bar", len(server.InstallHint)) | ||
| 120 | + } | ||
| 121 | +} | ||
| 122 | + | ||
| 123 | +func TestServerArgsCannotBeAppendedToByACaller(t *testing.T) { | ||
| 124 | + first := gololang.ServerArgs() | ||
| 125 | + first = append(first, "--nonsense") | ||
| 126 | + | ||
| 127 | + if second := gololang.ServerArgs(); slices.Contains(second, "--nonsense") { | ||
| 128 | + t.Errorf("ServerArgs() = %v after a caller appended to an earlier result", second) | ||
| 129 | + } | ||
| 130 | +} | ||
| 131 | + | ||
| 132 | +func TestServerIsLookedForWhereGoloScriptInstallsIt(t *testing.T) { | ||
| 133 | + // GoloScript's install.sh copies golo, gogolo and wagolo into | ||
| 134 | + // /usr/local/bin. It is on PATH on nearly every machine, and the editor | ||
| 135 | + // searches it anyway for the one where it is not. | ||
| 136 | + dirs := gololang.Profile().Server.Dirs | ||
| 137 | + | ||
| 138 | + if !slices.Contains(dirs, "/usr/local/bin") { | ||
| 139 | + t.Errorf("Server.Dirs = %v, want it to contain /usr/local/bin", dirs) | ||
| 140 | + } | ||
| 141 | + if len(dirs) != 1 { | ||
| 142 | + t.Errorf("Server.Dirs = %v, want the one directory the installer writes into and no guesses", dirs) | ||
| 143 | + } | ||
| 144 | +} | ||
| 145 | + | ||
| 146 | +func TestProfileHandsOutAFreshCopyEveryTime(t *testing.T) { | ||
| 147 | + // A caller that appends to Server.Dirs must not be appending to the | ||
| 148 | + // package's one copy, or the next caller would inherit its guess. | ||
| 149 | + first := gololang.Profile() | ||
| 150 | + first.Server.Dirs = append(first.Server.Dirs, "/somewhere/else") | ||
| 151 | + | ||
| 152 | + if second := gololang.Profile(); slices.Contains(second.Server.Dirs, "/somewhere/else") { | ||
| 153 | + t.Errorf("Profile().Server.Dirs = %v after a caller appended to an earlier result", second.Server.Dirs) | ||
| 154 | + } | ||
| 155 | +} | ||
| 156 | + | ||
| 157 | +func TestTemplatesAreAllFilledIn(t *testing.T) { | ||
| 158 | + templates := gololang.Profile().Templates | ||
| 159 | + | ||
| 160 | + for name, template := range map[string]string{ | ||
| 161 | + "Settings": templates.Settings, | ||
| 162 | + "Snippets": templates.Snippets, | ||
| 163 | + "Tools": templates.Tools, | ||
| 164 | + } { | ||
| 165 | + if template == "" { | ||
| 166 | + t.Errorf("Templates.%s is empty; the editor would offer to write nothing", name) | ||
| 167 | + } | ||
| 168 | + } | ||
| 169 | +} | ||
| 170 | + | ||
| 171 | +func TestRegisterTeachesTheLibraryGolo(t *testing.T) { | ||
| 172 | + gololang.Register() | ||
| 173 | + | ||
| 174 | + if !slices.Contains(syntax.Registered(), gololang.Language) { | ||
| 175 | + t.Fatalf("Registered() = %v, want it to contain %q", syntax.Registered(), gololang.Language) | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + for _, path := range []string{"main.golo", "lib/strings_test.golo", "DEEP/nested/x.GOLO"} { | ||
| 179 | + if got := syntax.LanguageOf(path, ""); got != gololang.Language { | ||
| 180 | + t.Errorf("LanguageOf(%q) = %q, want %q", path, got, gololang.Language) | ||
| 181 | + } | ||
| 182 | + } | ||
| 183 | +} | ||
| 184 | + | ||
| 185 | +func TestAShebangNamingGoloIsGolo(t *testing.T) { | ||
| 186 | + // A script run as a command has no extension and opens with a line the | ||
| 187 | + // interpreter reads as a comment. The first line is what identifies it. | ||
| 188 | + gololang.Register() | ||
| 189 | + | ||
| 190 | + for _, firstLine := range []string{"#!/usr/bin/env golo", "#!/usr/local/bin/golo"} { | ||
| 191 | + if got := syntax.LanguageOf("run-me", firstLine); got != gololang.Language { | ||
| 192 | + t.Errorf("LanguageOf(%q) = %q, want %q", firstLine, got, gololang.Language) | ||
| 193 | + } | ||
| 194 | + } | ||
| 195 | +} | ||
| 196 | + | ||
| 197 | +func TestRegisterLeavesOtherFilesAlone(t *testing.T) { | ||
| 198 | + gololang.Register() | ||
| 199 | + | ||
| 200 | + cases := map[string]syntax.Language{ | ||
| 201 | + "README.md": syntax.LanguageMarkdown, | ||
| 202 | + "Dockerfile": syntax.LanguageDockerfile, | ||
| 203 | + "main.py": syntax.LanguageNone, | ||
| 204 | + "main.mbt": syntax.LanguageNone, | ||
| 205 | + // A golo file's neighbour with a different extension is not Golo | ||
| 206 | + // however Golo-flavoured its name. | ||
| 207 | + "golo.toml": syntax.LanguageTOML, | ||
| 208 | + } | ||
| 209 | + for path, want := range cases { | ||
| 210 | + if got := syntax.LanguageOf(path, ""); got != want { | ||
| 211 | + t.Errorf("LanguageOf(%q) = %q, want %q", path, got, want) | ||
| 212 | + } | ||
| 213 | + } | ||
| 214 | + // A shebang naming another interpreter is not Golo either. | ||
| 215 | + if got := syntax.LanguageOf("script", "#!/usr/bin/env python3"); got == gololang.Language { | ||
| 216 | + t.Errorf("LanguageOf with a python shebang = %q, want anything but Golo", got) | ||
| 217 | + } | ||
| 218 | +} | ||
| 219 | + | ||
| 220 | +// A profile is a literal, so this example is the whole of how one is used. | ||
| 221 | +func ExampleProfile() { | ||
| 222 | + gololang.Register() | ||
| 223 | + | ||
| 224 | + var p profile.Profile = gololang.Profile() | ||
| 225 | + _ = p.ProjectDir() // ".turbo-golo" | ||
| 226 | +} | ||
added
internal/gololang/reference_test.go +199 -0 | new file mode 100644 | ||
| @@ -0,0 +1,199 @@ | ||
| 1 | +package gololang_test | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "os" | |
| 5 | + "strings" | |
| 6 | + "testing" | |
| 7 | + | |
| 8 | + "rickub.com/turbo-editors/turbo-core/syntax" | |
| 9 | + | |
| 10 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | |
| 11 | +) | |
| 12 | + | |
| 13 | +// TestTheLanguagesReferenceIsTrue holds docs/*/reference/languages.md to the | |
| 14 | +// scanner. Every row of its Golo table that no other test here covers is | |
| 15 | +// checked, so a reference claim and the code cannot drift apart quietly. | |
| 16 | +// | |
| 17 | +// The last rows document *limitations* rather than features — a constructor | |
| 18 | +// read as a type, a digit separator that is not one, a keyword after a colon. | |
| 19 | +// The reference says each of those in so many words, and these rows are what | |
| 20 | +// stops somebody "fixing" one without also fixing the sentence. | |
| 21 | +func TestTheLanguagesReferenceIsTrue(t *testing.T) { | |
| 22 | + tests := []struct { | |
| 23 | + src string | |
| 24 | + word string | |
| 25 | + want syntax.Class | |
| 26 | + }{ | |
| 27 | + // Keywords the reference lists that no other test exercises. | |
| 28 | + {"augmentation Named = {", "augmentation", syntax.ClassKeyword}, | |
| 29 | + {"await task", "await", syntax.ClassKeyword}, | |
| 30 | + {"case x {", "case", syntax.ClassKeyword}, | |
| 31 | + {"x isnt null", "isnt", syntax.ClassKeyword}, | |
| 32 | + {"spawn { work() }", "spawn", syntax.ClassKeyword}, | |
| 33 | + {"augment Dog with Runnable", "with", syntax.ClassKeyword}, | |
| 34 | + {"while running {", "while", syntax.ClassKeyword}, | |
| 35 | + {"otherwise 0", "otherwise", syntax.ClassKeyword}, | |
| 36 | + | |
| 37 | + // Constants and builtins the reference names. | |
| 38 | + {"let x = false", "false", syntax.ClassConstant}, | |
| 39 | + {"print(x)", "print", syntax.ClassBuiltin}, | |
| 40 | + {"str(x)", "str", syntax.ClassBuiltin}, | |
| 41 | + {"len(xs)", "len", syntax.ClassBuiltin}, | |
| 42 | + {"map[[1, 2]]", "map", syntax.ClassBuiltin}, | |
| 43 | + {"set[1, 2]", "set", syntax.ClassBuiltin}, | |
| 44 | + {"array[1, 2]", "array", syntax.ClassBuiltin}, | |
| 45 | + {"vector[1, 2]", "vector", syntax.ClassBuiltin}, | |
| 46 | + {"readFile(path)", "readFile", syntax.ClassBuiltin}, | |
| 47 | + {"toJSON(x)", "toJSON", syntax.ClassBuiltin}, | |
| 48 | + {"fromJSON(x)", "fromJSON", syntax.ClassBuiltin}, | |
| 49 | + {"httpGet(url)", "httpGet", syntax.ClassBuiltin}, | |
| 50 | + {"DynamicObject()", "DynamicObject", syntax.ClassBuiltin}, | |
| 51 | + | |
| 52 | + // Types, by convention. | |
| 53 | + {"let s = Some(1)", "Some", syntax.ClassType}, | |
| 54 | + {"let r = Result_Failure(\"no\")", "Result_Failure", syntax.ClassType}, | |
| 55 | + {"import java.util.List", "java.util.List", syntax.ClassType}, | |
| 56 | + | |
| 57 | + // Names. | |
| 58 | + {"let été = 1", "été", syntax.ClassIdentifier}, | |
| 59 | + {"let 名前 = 1", "名前", syntax.ClassIdentifier}, | |
| 60 | + {"function 🚀launch = {", "🚀launch", syntax.ClassFunction}, | |
| 61 | + | |
| 62 | + // Literals and numbers, one per item of the reference's list. | |
| 63 | + {"let s = \"a\\x41b\"", "\"a\\x41b\"", syntax.ClassString}, | |
| 64 | + {"let s = \"\"\"raw\"\"\"", "\"\"\"raw\"\"\"", syntax.ClassString}, | |
| 65 | + {"let c = '\\''", "'\\''", syntax.ClassChar}, | |
| 66 | + {"let n = 2E10", "2E10", syntax.ClassNumber}, | |
| 67 | + {"let n = 3.14F", "3.14F", syntax.ClassNumber}, | |
| 68 | + {"let n = 2.0f", "2.0f", syntax.ClassNumber}, | |
| 69 | + {"let n = 1e", "1e", syntax.ClassNumber}, | |
| 70 | + | |
| 71 | + // Comments, operators, punctuation. | |
| 72 | + {"#!/usr/bin/env golo", "#!/usr/bin/env golo", syntax.ClassComment}, | |
| 73 | + {"---- one line ----", "---- one line ----", syntax.ClassComment}, | |
| 74 | + {"a >= b", ">=", syntax.ClassOperator}, | |
| 75 | + {"a == b", "==", syntax.ClassOperator}, | |
| 76 | + {"f(a...)", "...", syntax.ClassOperator}, | |
| 77 | + {"x; y", ";", syntax.ClassPunctuation}, | |
| 78 | + | |
| 79 | + // The documented limitations. | |
| 80 | + {"let n = 1_000", "1", syntax.ClassNumber}, | |
| 81 | + {"let n = 0b1010", "b1010", syntax.ClassIdentifier}, | |
| 82 | + {"let n = 0o17", "o17", syntax.ClassIdentifier}, | |
| 83 | + {"let n = .5", ".", syntax.ClassPunctuation}, | |
| 84 | + {"x = 42l", "l", syntax.ClassIdentifier}, | |
| 85 | + {"a --- b", "---", syntax.ClassOperator}, | |
| 86 | + {"obj: match()", "match", syntax.ClassKeyword}, | |
| 87 | + {"let c = Circle(1.0)", "Circle", syntax.ClassType}, | |
| 88 | + {"let Count = 1", "Count", syntax.ClassType}, | |
| 89 | + } | |
| 90 | + | |
| 91 | + for _, test := range tests { | |
| 92 | + t.Run(test.word, func(t *testing.T) { | |
| 93 | + index := len([]rune(test.src[:strings.Index(test.src, test.word)])) | |
| 94 | + if strings.Index(test.src, test.word) < 0 { | |
| 95 | + t.Fatalf("%q not in %q", test.word, test.src) | |
| 96 | + } | |
| 97 | + got, ok := classAt(gololang.Highlight(test.src), 0, index) | |
| 98 | + if !ok || got != test.want { | |
| 99 | + t.Errorf("%q in %q is %v (covered %v), want %v", test.word, test.src, got, ok, test.want) | |
| 100 | + } | |
| 101 | + }) | |
| 102 | + } | |
| 103 | +} | |
| 104 | + | |
| 105 | +// classAt returns the class covering one rune column of one line. | |
| 106 | +func classAt(spans [][]syntax.Span, line, col int) (syntax.Class, bool) { | |
| 107 | + if line < 0 || line >= len(spans) { | |
| 108 | + return 0, false | |
| 109 | + } | |
| 110 | + for _, s := range spans[line] { | |
| 111 | + if col >= s.Start && col < s.End { | |
| 112 | + return s.Class, true | |
| 113 | + } | |
| 114 | + } | |
| 115 | + return 0, false | |
| 116 | +} | |
| 117 | + | |
| 118 | +// The reference's Classes table says Golo produces no heading, tag, attribute, | |
| 119 | +// emphasis or link. That is a claim about every span the scanner can ever | |
| 120 | +// emit, so it is checked over a file that uses every construct the Golo table | |
| 121 | +// names. | |
| 122 | +func TestTheScannerNeverProducesAMarkupClass(t *testing.T) { | |
| 123 | + const src = "#!/usr/bin/env golo\n" + | |
| 124 | + "module demo.Tour\n" + | |
| 125 | + "import gololang.Errors\n" + | |
| 126 | + "---- a block\ncomment ----\n" + | |
| 127 | + "struct Point = { x, y }\n" + | |
| 128 | + "union Shape = { Circle = { radius } }\n" + | |
| 129 | + "augment Shape$Circle { function d = |this| -> this: radius() * 2 }\n" + | |
| 130 | + "function main = |args| {\n" + | |
| 131 | + " let n = 42L + 3.14F + 1.5e-3\n" + | |
| 132 | + " let s = \"a \\\"b\\\" \\x41\" + \"\"\"multi\n\"quoted\"\n\"\"\" + 'c'\n" + | |
| 133 | + " let 😀 = list[1..3, x...]\n" + | |
| 134 | + " let ok = p?: x() orIfNull 0 # trailing\n" + | |
| 135 | + " let broken = \"unterminated\n" + | |
| 136 | + "}\n" | |
| 137 | + | |
| 138 | + forbidden := map[syntax.Class]bool{ | |
| 139 | + syntax.ClassHeading: true, | |
| 140 | + syntax.ClassTag: true, | |
| 141 | + syntax.ClassAttribute: true, | |
| 142 | + syntax.ClassEmphasis: true, | |
| 143 | + syntax.ClassLink: true, | |
| 144 | + } | |
| 145 | + for line, spans := range gololang.Highlight(src) { | |
| 146 | + for _, span := range spans { | |
| 147 | + if forbidden[span.Class] { | |
| 148 | + t.Errorf("line %d holds a %s span at %d; the reference says Golo produces none", line+1, span.Class, span.Start) | |
| 149 | + } | |
| 150 | + } | |
| 151 | + } | |
| 152 | +} | |
| 153 | + | |
| 154 | +// Every keyword the reference's table lists is one the scanner really treats as | |
| 155 | +// a keyword, and every keyword the scanner knows is in the table. The table is | |
| 156 | +// read out of the page rather than repeated here, so a word added to one and | |
| 157 | +// not the other is what fails. | |
| 158 | +func TestEveryKeywordTheReferenceListsIsAKeyword(t *testing.T) { | |
| 159 | + for _, page := range []string{"../../docs/en/reference/languages.md", "../../docs/fr/reference/languages.md"} { | |
| 160 | + raw, err := os.ReadFile(page) | |
| 161 | + if err != nil { | |
| 162 | + t.Fatalf("reading %s: %v", page, err) | |
| 163 | + } | |
| 164 | + | |
| 165 | + words := keywordRowOf(t, string(raw)) | |
| 166 | + if len(words) != len(gololang.Keywords()) { | |
| 167 | + t.Errorf("%s lists %d keywords; the scanner knows %d", page, len(words), len(gololang.Keywords())) | |
| 168 | + } | |
| 169 | + for _, word := range words { | |
| 170 | + src := word + " x" | |
| 171 | + got, ok := classAt(gololang.Highlight(src), 0, 0) | |
| 172 | + if !ok || got != syntax.ClassKeyword { | |
| 173 | + t.Errorf("%s says %q is a keyword; the scanner colours it %v", page, word, got) | |
| 174 | + } | |
| 175 | + } | |
| 176 | + } | |
| 177 | +} | |
| 178 | + | |
| 179 | +// keywordRowOf pulls the back-quoted words out of the reference's keyword row. | |
| 180 | +func keywordRowOf(t *testing.T, page string) []string { | |
| 181 | + t.Helper() | |
| 182 | + | |
| 183 | + for _, line := range strings.Split(page, "\n") { | |
| 184 | + if !strings.Contains(line, "`function`") || !strings.Contains(line, "`orIfNull`") { | |
| 185 | + continue | |
| 186 | + } | |
| 187 | + var words []string | |
| 188 | + for i, part := range strings.Split(line, "`") { | |
| 189 | + if i%2 == 1 { | |
| 190 | + words = append(words, part) | |
| 191 | + } | |
| 192 | + } | |
| 193 | + // The row ends "| keyword |" in English and "| mot-clé |" in French; | |
| 194 | + // the back-quoted words are the same in both. | |
| 195 | + return words | |
| 196 | + } | |
| 197 | + t.Fatal("no keyword row found in the reference") | |
| 198 | + return nil | |
| 199 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,199 @@ | |||
| 1 | +package gololang_test | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "os" | ||
| 5 | + "strings" | ||
| 6 | + "testing" | ||
| 7 | + | ||
| 8 | + "rickub.com/turbo-editors/turbo-core/syntax" | ||
| 9 | + | ||
| 10 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | ||
| 11 | +) | ||
| 12 | + | ||
| 13 | +// TestTheLanguagesReferenceIsTrue holds docs/*/reference/languages.md to the | ||
| 14 | +// scanner. Every row of its Golo table that no other test here covers is | ||
| 15 | +// checked, so a reference claim and the code cannot drift apart quietly. | ||
| 16 | +// | ||
| 17 | +// The last rows document *limitations* rather than features — a constructor | ||
| 18 | +// read as a type, a digit separator that is not one, a keyword after a colon. | ||
| 19 | +// The reference says each of those in so many words, and these rows are what | ||
| 20 | +// stops somebody "fixing" one without also fixing the sentence. | ||
| 21 | +func TestTheLanguagesReferenceIsTrue(t *testing.T) { | ||
| 22 | + tests := []struct { | ||
| 23 | + src string | ||
| 24 | + word string | ||
| 25 | + want syntax.Class | ||
| 26 | + }{ | ||
| 27 | + // Keywords the reference lists that no other test exercises. | ||
| 28 | + {"augmentation Named = {", "augmentation", syntax.ClassKeyword}, | ||
| 29 | + {"await task", "await", syntax.ClassKeyword}, | ||
| 30 | + {"case x {", "case", syntax.ClassKeyword}, | ||
| 31 | + {"x isnt null", "isnt", syntax.ClassKeyword}, | ||
| 32 | + {"spawn { work() }", "spawn", syntax.ClassKeyword}, | ||
| 33 | + {"augment Dog with Runnable", "with", syntax.ClassKeyword}, | ||
| 34 | + {"while running {", "while", syntax.ClassKeyword}, | ||
| 35 | + {"otherwise 0", "otherwise", syntax.ClassKeyword}, | ||
| 36 | + | ||
| 37 | + // Constants and builtins the reference names. | ||
| 38 | + {"let x = false", "false", syntax.ClassConstant}, | ||
| 39 | + {"print(x)", "print", syntax.ClassBuiltin}, | ||
| 40 | + {"str(x)", "str", syntax.ClassBuiltin}, | ||
| 41 | + {"len(xs)", "len", syntax.ClassBuiltin}, | ||
| 42 | + {"map[[1, 2]]", "map", syntax.ClassBuiltin}, | ||
| 43 | + {"set[1, 2]", "set", syntax.ClassBuiltin}, | ||
| 44 | + {"array[1, 2]", "array", syntax.ClassBuiltin}, | ||
| 45 | + {"vector[1, 2]", "vector", syntax.ClassBuiltin}, | ||
| 46 | + {"readFile(path)", "readFile", syntax.ClassBuiltin}, | ||
| 47 | + {"toJSON(x)", "toJSON", syntax.ClassBuiltin}, | ||
| 48 | + {"fromJSON(x)", "fromJSON", syntax.ClassBuiltin}, | ||
| 49 | + {"httpGet(url)", "httpGet", syntax.ClassBuiltin}, | ||
| 50 | + {"DynamicObject()", "DynamicObject", syntax.ClassBuiltin}, | ||
| 51 | + | ||
| 52 | + // Types, by convention. | ||
| 53 | + {"let s = Some(1)", "Some", syntax.ClassType}, | ||
| 54 | + {"let r = Result_Failure(\"no\")", "Result_Failure", syntax.ClassType}, | ||
| 55 | + {"import java.util.List", "java.util.List", syntax.ClassType}, | ||
| 56 | + | ||
| 57 | + // Names. | ||
| 58 | + {"let été = 1", "été", syntax.ClassIdentifier}, | ||
| 59 | + {"let 名前 = 1", "名前", syntax.ClassIdentifier}, | ||
| 60 | + {"function 🚀launch = {", "🚀launch", syntax.ClassFunction}, | ||
| 61 | + | ||
| 62 | + // Literals and numbers, one per item of the reference's list. | ||
| 63 | + {"let s = \"a\\x41b\"", "\"a\\x41b\"", syntax.ClassString}, | ||
| 64 | + {"let s = \"\"\"raw\"\"\"", "\"\"\"raw\"\"\"", syntax.ClassString}, | ||
| 65 | + {"let c = '\\''", "'\\''", syntax.ClassChar}, | ||
| 66 | + {"let n = 2E10", "2E10", syntax.ClassNumber}, | ||
| 67 | + {"let n = 3.14F", "3.14F", syntax.ClassNumber}, | ||
| 68 | + {"let n = 2.0f", "2.0f", syntax.ClassNumber}, | ||
| 69 | + {"let n = 1e", "1e", syntax.ClassNumber}, | ||
| 70 | + | ||
| 71 | + // Comments, operators, punctuation. | ||
| 72 | + {"#!/usr/bin/env golo", "#!/usr/bin/env golo", syntax.ClassComment}, | ||
| 73 | + {"---- one line ----", "---- one line ----", syntax.ClassComment}, | ||
| 74 | + {"a >= b", ">=", syntax.ClassOperator}, | ||
| 75 | + {"a == b", "==", syntax.ClassOperator}, | ||
| 76 | + {"f(a...)", "...", syntax.ClassOperator}, | ||
| 77 | + {"x; y", ";", syntax.ClassPunctuation}, | ||
| 78 | + | ||
| 79 | + // The documented limitations. | ||
| 80 | + {"let n = 1_000", "1", syntax.ClassNumber}, | ||
| 81 | + {"let n = 0b1010", "b1010", syntax.ClassIdentifier}, | ||
| 82 | + {"let n = 0o17", "o17", syntax.ClassIdentifier}, | ||
| 83 | + {"let n = .5", ".", syntax.ClassPunctuation}, | ||
| 84 | + {"x = 42l", "l", syntax.ClassIdentifier}, | ||
| 85 | + {"a --- b", "---", syntax.ClassOperator}, | ||
| 86 | + {"obj: match()", "match", syntax.ClassKeyword}, | ||
| 87 | + {"let c = Circle(1.0)", "Circle", syntax.ClassType}, | ||
| 88 | + {"let Count = 1", "Count", syntax.ClassType}, | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + for _, test := range tests { | ||
| 92 | + t.Run(test.word, func(t *testing.T) { | ||
| 93 | + index := len([]rune(test.src[:strings.Index(test.src, test.word)])) | ||
| 94 | + if strings.Index(test.src, test.word) < 0 { | ||
| 95 | + t.Fatalf("%q not in %q", test.word, test.src) | ||
| 96 | + } | ||
| 97 | + got, ok := classAt(gololang.Highlight(test.src), 0, index) | ||
| 98 | + if !ok || got != test.want { | ||
| 99 | + t.Errorf("%q in %q is %v (covered %v), want %v", test.word, test.src, got, ok, test.want) | ||
| 100 | + } | ||
| 101 | + }) | ||
| 102 | + } | ||
| 103 | +} | ||
| 104 | + | ||
| 105 | +// classAt returns the class covering one rune column of one line. | ||
| 106 | +func classAt(spans [][]syntax.Span, line, col int) (syntax.Class, bool) { | ||
| 107 | + if line < 0 || line >= len(spans) { | ||
| 108 | + return 0, false | ||
| 109 | + } | ||
| 110 | + for _, s := range spans[line] { | ||
| 111 | + if col >= s.Start && col < s.End { | ||
| 112 | + return s.Class, true | ||
| 113 | + } | ||
| 114 | + } | ||
| 115 | + return 0, false | ||
| 116 | +} | ||
| 117 | + | ||
| 118 | +// The reference's Classes table says Golo produces no heading, tag, attribute, | ||
| 119 | +// emphasis or link. That is a claim about every span the scanner can ever | ||
| 120 | +// emit, so it is checked over a file that uses every construct the Golo table | ||
| 121 | +// names. | ||
| 122 | +func TestTheScannerNeverProducesAMarkupClass(t *testing.T) { | ||
| 123 | + const src = "#!/usr/bin/env golo\n" + | ||
| 124 | + "module demo.Tour\n" + | ||
| 125 | + "import gololang.Errors\n" + | ||
| 126 | + "---- a block\ncomment ----\n" + | ||
| 127 | + "struct Point = { x, y }\n" + | ||
| 128 | + "union Shape = { Circle = { radius } }\n" + | ||
| 129 | + "augment Shape$Circle { function d = |this| -> this: radius() * 2 }\n" + | ||
| 130 | + "function main = |args| {\n" + | ||
| 131 | + " let n = 42L + 3.14F + 1.5e-3\n" + | ||
| 132 | + " let s = \"a \\\"b\\\" \\x41\" + \"\"\"multi\n\"quoted\"\n\"\"\" + 'c'\n" + | ||
| 133 | + " let 😀 = list[1..3, x...]\n" + | ||
| 134 | + " let ok = p?: x() orIfNull 0 # trailing\n" + | ||
| 135 | + " let broken = \"unterminated\n" + | ||
| 136 | + "}\n" | ||
| 137 | + | ||
| 138 | + forbidden := map[syntax.Class]bool{ | ||
| 139 | + syntax.ClassHeading: true, | ||
| 140 | + syntax.ClassTag: true, | ||
| 141 | + syntax.ClassAttribute: true, | ||
| 142 | + syntax.ClassEmphasis: true, | ||
| 143 | + syntax.ClassLink: true, | ||
| 144 | + } | ||
| 145 | + for line, spans := range gololang.Highlight(src) { | ||
| 146 | + for _, span := range spans { | ||
| 147 | + if forbidden[span.Class] { | ||
| 148 | + t.Errorf("line %d holds a %s span at %d; the reference says Golo produces none", line+1, span.Class, span.Start) | ||
| 149 | + } | ||
| 150 | + } | ||
| 151 | + } | ||
| 152 | +} | ||
| 153 | + | ||
| 154 | +// Every keyword the reference's table lists is one the scanner really treats as | ||
| 155 | +// a keyword, and every keyword the scanner knows is in the table. The table is | ||
| 156 | +// read out of the page rather than repeated here, so a word added to one and | ||
| 157 | +// not the other is what fails. | ||
| 158 | +func TestEveryKeywordTheReferenceListsIsAKeyword(t *testing.T) { | ||
| 159 | + for _, page := range []string{"../../docs/en/reference/languages.md", "../../docs/fr/reference/languages.md"} { | ||
| 160 | + raw, err := os.ReadFile(page) | ||
| 161 | + if err != nil { | ||
| 162 | + t.Fatalf("reading %s: %v", page, err) | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + words := keywordRowOf(t, string(raw)) | ||
| 166 | + if len(words) != len(gololang.Keywords()) { | ||
| 167 | + t.Errorf("%s lists %d keywords; the scanner knows %d", page, len(words), len(gololang.Keywords())) | ||
| 168 | + } | ||
| 169 | + for _, word := range words { | ||
| 170 | + src := word + " x" | ||
| 171 | + got, ok := classAt(gololang.Highlight(src), 0, 0) | ||
| 172 | + if !ok || got != syntax.ClassKeyword { | ||
| 173 | + t.Errorf("%s says %q is a keyword; the scanner colours it %v", page, word, got) | ||
| 174 | + } | ||
| 175 | + } | ||
| 176 | + } | ||
| 177 | +} | ||
| 178 | + | ||
| 179 | +// keywordRowOf pulls the back-quoted words out of the reference's keyword row. | ||
| 180 | +func keywordRowOf(t *testing.T, page string) []string { | ||
| 181 | + t.Helper() | ||
| 182 | + | ||
| 183 | + for _, line := range strings.Split(page, "\n") { | ||
| 184 | + if !strings.Contains(line, "`function`") || !strings.Contains(line, "`orIfNull`") { | ||
| 185 | + continue | ||
| 186 | + } | ||
| 187 | + var words []string | ||
| 188 | + for i, part := range strings.Split(line, "`") { | ||
| 189 | + if i%2 == 1 { | ||
| 190 | + words = append(words, part) | ||
| 191 | + } | ||
| 192 | + } | ||
| 193 | + // The row ends "| keyword |" in English and "| mot-clé |" in French; | ||
| 194 | + // the back-quoted words are the same in both. | ||
| 195 | + return words | ||
| 196 | + } | ||
| 197 | + t.Fatal("no keyword row found in the reference") | ||
| 198 | + return nil | ||
| 199 | +} | ||
added
internal/gololang/scan.go +165 -0 | new file mode 100644 | ||
| @@ -0,0 +1,165 @@ | ||
| 1 | +package gololang | |
| 2 | + | |
| 3 | +import "rickub.com/turbo-editors/turbo-core/syntax" | |
| 4 | + | |
| 5 | +// carry is what a line of Golo leaves open for the next one: which construct, | |
| 6 | +// if any, the line ended inside. | |
| 7 | +// | |
| 8 | +// Four constructs may reach the next line, and the interpreter's own lexer is | |
| 9 | +// the authority on each. A block comment runs from one ---- to the next | |
| 10 | +// wherever that is. A "string", a """multi-line string""" and a 'character' | |
| 11 | +// are all read to their closing quote, and that quote may be on a later line: | |
| 12 | +// lexer.go reads a plain string with `for l.ch != '"' && l.ch != 0`, which | |
| 13 | +// stops at the quote or the end of the file and at nothing in between. So an | |
| 14 | +// unterminated string paints the rest of the file — and that is what the | |
| 15 | +// interpreter does with it too, which is the reason to carry rather than to | |
| 16 | +// stop at the line the way Turbo MoonBit does for a language whose grammar | |
| 17 | +// forbids the newline. | |
| 18 | +// | |
| 19 | +// None of the four nests, so a value saying which one is open is the whole of | |
| 20 | +// the state. A depth would be a claim the language does not make. | |
| 21 | +type carry int | |
| 22 | + | |
| 23 | +const ( | |
| 24 | + // nothingOpen is the state between constructs, and the zero value | |
| 25 | + // ScanLines starts the first line in. | |
| 26 | + nothingOpen carry = iota | |
| 27 | + // blockCommentOpen means a ---- was seen and its closing ---- was not. | |
| 28 | + blockCommentOpen | |
| 29 | + // stringOpen means a " was seen and its closing " was not. | |
| 30 | + stringOpen | |
| 31 | + // multilineStringOpen means a """ was seen and its closing """ was not. | |
| 32 | + multilineStringOpen | |
| 33 | + // charOpen means a ' was seen and its closing ' was not. | |
| 34 | + charOpen | |
| 35 | +) | |
| 36 | + | |
| 37 | +// Highlight colours Golo source. | |
| 38 | +// | |
| 39 | +// It is written against syntax.LineScanner, a line at a time, with the | |
| 40 | +// interpreter's lexer/lexer.go as its specification. GoloScript's own lexer is | |
| 41 | +// a Go package, but its module is named `golo` and is not importable from | |
| 42 | +// another module, so the rules are carried here rather than called — and what | |
| 43 | +// the lexer reads as one token, this scanner colours as one span. | |
| 44 | +// | |
| 45 | +// It is deliberately tolerant of broken input: source under the cursor is | |
| 46 | +// invalid most of the time it is being typed, and a highlighter that gives up | |
| 47 | +// is a highlighter that flickers off. | |
| 48 | +// | |
| 49 | +// spans := gololang.Highlight("function main = |args| {\n println(\"hi\")\n}\n") | |
| 50 | +// // spans[0][0] covers "function" with syntax.ClassKeyword | |
| 51 | +func Highlight(src string) [][]syntax.Span { | |
| 52 | + return syntax.ScanLines(src, scanLine) | |
| 53 | +} | |
| 54 | + | |
| 55 | +// scanLine colours one line, finishing whatever the previous line left open | |
| 56 | +// before looking at anything new, and reports what this line leaves open. | |
| 57 | +func scanLine(line []rune, open carry) ([]syntax.Span, carry) { | |
| 58 | + s := syntax.NewLineScanner(line) | |
| 59 | + | |
| 60 | + open = finishOpen(s, open) | |
| 61 | + for !s.AtEnd() { | |
| 62 | + open = scanToken(s) | |
| 63 | + } | |
| 64 | + return s.Spans(), open | |
| 65 | +} | |
| 66 | + | |
| 67 | +// finishOpen colours the continuation of a construct opened on an earlier | |
| 68 | +// line, and says whether it is still open at the end of this one. With nothing | |
| 69 | +// open it does nothing. | |
| 70 | +func finishOpen(s *syntax.LineScanner, open carry) carry { | |
| 71 | + switch open { | |
| 72 | + case blockCommentOpen: | |
| 73 | + return stillOpen(syntax.FinishBlockComment(s, blockCommentMarker, syntax.ClassComment), open) | |
| 74 | + case multilineStringOpen: | |
| 75 | + return stillOpen(syntax.FinishBlockComment(s, multilineStringQuote, syntax.ClassString), open) | |
| 76 | + case stringOpen: | |
| 77 | + return finishQuoted(s, '"', syntax.ClassString, open) | |
| 78 | + case charOpen: | |
| 79 | + return finishQuoted(s, '\'', syntax.ClassChar, open) | |
| 80 | + default: | |
| 81 | + return nothingOpen | |
| 82 | + } | |
| 83 | +} | |
| 84 | + | |
| 85 | +// stillOpen turns "did it close?" into what the next line should be told. | |
| 86 | +func stillOpen(closed bool, open carry) carry { | |
| 87 | + if closed { | |
| 88 | + return nothingOpen | |
| 89 | + } | |
| 90 | + return open | |
| 91 | +} | |
| 92 | + | |
| 93 | +// blockCommentMarker opens and closes a block comment. The lexer asks for four | |
| 94 | +// dashes exactly: three are an operator run, and a fifth is part of the text. | |
| 95 | +const blockCommentMarker = "----" | |
| 96 | + | |
| 97 | +// scanToken colours whatever starts at the scanner's position, and reports | |
| 98 | +// which construct, if any, ran off the end of the line. | |
| 99 | +// | |
| 100 | +// The order of the cases is the design, and three of them are load-bearing: | |
| 101 | +// | |
| 102 | +// - ---- is tested before the operators, because - is an operator rune and | |
| 103 | +// the run would otherwise be coloured as one. | |
| 104 | +// - """ is tested before ", because both begin with a quote and what tells | |
| 105 | +// them apart is the two runes after it. The lexer makes the same check in | |
| 106 | +// the same order. | |
| 107 | +// - A digit is tested before a word, and a word before a dot, so that 1.5 is | |
| 108 | +// one number and hello.World is a name, a dot and a name. | |
| 109 | +func scanToken(s *syntax.LineScanner) carry { | |
| 110 | + r := s.Peek(0) | |
| 111 | + | |
| 112 | + switch { | |
| 113 | + case r == ' ' || r == '\t': | |
| 114 | + s.SkipSpaces() | |
| 115 | + case r == '#': | |
| 116 | + // # opens a comment that runs to the end of the line. A shebang is | |
| 117 | + // one too: #!/usr/bin/env golo is how a script is run as a command, | |
| 118 | + // and the interpreter reads it as a comment because that is what it | |
| 119 | + // is. | |
| 120 | + s.TakeRest(syntax.ClassComment) | |
| 121 | + case s.HasPrefix(0, blockCommentMarker): | |
| 122 | + return stillOpen(syntax.OpenBlockComment(s, blockCommentMarker, blockCommentMarker, syntax.ClassComment), blockCommentOpen) | |
| 123 | + case s.HasPrefix(0, multilineStringQuote): | |
| 124 | + return takeMultilineString(s) | |
| 125 | + case r == '"': | |
| 126 | + return takeQuoted(s, '"', syntax.ClassString, stringOpen) | |
| 127 | + case r == '\'': | |
| 128 | + return takeQuoted(s, '\'', syntax.ClassChar, charOpen) | |
| 129 | + case syntax.IsDigit(r): | |
| 130 | + takeNumber(s) | |
| 131 | + case isIdentifierStart(r): | |
| 132 | + takeWord(s) | |
| 133 | + case r == '.': | |
| 134 | + takeDot(s) | |
| 135 | + case r == '$': | |
| 136 | + // $ joins a union to one of its variants in `augment Shape$Circle`. | |
| 137 | + // It is structure rather than computation, so it is punctuation like | |
| 138 | + // the dot in a module path. | |
| 139 | + s.Take(1, syntax.ClassPunctuation) | |
| 140 | + case syntax.IsOperatorRune(r): | |
| 141 | + s.TakeWhile(syntax.ClassOperator, syntax.IsOperatorRune) | |
| 142 | + case syntax.IsPunctuationRune(r): | |
| 143 | + s.Take(1, syntax.ClassPunctuation) | |
| 144 | + default: | |
| 145 | + // A rune nothing here claims — a stray control character, a symbol | |
| 146 | + // outside the ranges the lexer accepts in a name — is stepped over | |
| 147 | + // uncoloured rather than guessed at. | |
| 148 | + s.Advance(1) | |
| 149 | + } | |
| 150 | + return nothingOpen | |
| 151 | +} | |
| 152 | + | |
| 153 | +// takeDot colours a dot and whatever the language says belongs with it. | |
| 154 | +// | |
| 155 | +// A run of dots is an operator — .. is a range and ... marks a variadic | |
| 156 | +// parameter — and a dot on its own is punctuation: the separator in a module | |
| 157 | +// path such as hello.World, or in a decimal that takeNumber did not claim | |
| 158 | +// because no digit came before it. | |
| 159 | +func takeDot(s *syntax.LineScanner) { | |
| 160 | + if s.Peek(1) == '.' { | |
| 161 | + s.TakeWhile(syntax.ClassOperator, func(r rune) bool { return r == '.' }) | |
| 162 | + return | |
| 163 | + } | |
| 164 | + s.Take(1, syntax.ClassPunctuation) | |
| 165 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,165 @@ | |||
| 1 | +package gololang | ||
| 2 | + | ||
| 3 | +import "rickub.com/turbo-editors/turbo-core/syntax" | ||
| 4 | + | ||
| 5 | +// carry is what a line of Golo leaves open for the next one: which construct, | ||
| 6 | +// if any, the line ended inside. | ||
| 7 | +// | ||
| 8 | +// Four constructs may reach the next line, and the interpreter's own lexer is | ||
| 9 | +// the authority on each. A block comment runs from one ---- to the next | ||
| 10 | +// wherever that is. A "string", a """multi-line string""" and a 'character' | ||
| 11 | +// are all read to their closing quote, and that quote may be on a later line: | ||
| 12 | +// lexer.go reads a plain string with `for l.ch != '"' && l.ch != 0`, which | ||
| 13 | +// stops at the quote or the end of the file and at nothing in between. So an | ||
| 14 | +// unterminated string paints the rest of the file — and that is what the | ||
| 15 | +// interpreter does with it too, which is the reason to carry rather than to | ||
| 16 | +// stop at the line the way Turbo MoonBit does for a language whose grammar | ||
| 17 | +// forbids the newline. | ||
| 18 | +// | ||
| 19 | +// None of the four nests, so a value saying which one is open is the whole of | ||
| 20 | +// the state. A depth would be a claim the language does not make. | ||
| 21 | +type carry int | ||
| 22 | + | ||
| 23 | +const ( | ||
| 24 | + // nothingOpen is the state between constructs, and the zero value | ||
| 25 | + // ScanLines starts the first line in. | ||
| 26 | + nothingOpen carry = iota | ||
| 27 | + // blockCommentOpen means a ---- was seen and its closing ---- was not. | ||
| 28 | + blockCommentOpen | ||
| 29 | + // stringOpen means a " was seen and its closing " was not. | ||
| 30 | + stringOpen | ||
| 31 | + // multilineStringOpen means a """ was seen and its closing """ was not. | ||
| 32 | + multilineStringOpen | ||
| 33 | + // charOpen means a ' was seen and its closing ' was not. | ||
| 34 | + charOpen | ||
| 35 | +) | ||
| 36 | + | ||
| 37 | +// Highlight colours Golo source. | ||
| 38 | +// | ||
| 39 | +// It is written against syntax.LineScanner, a line at a time, with the | ||
| 40 | +// interpreter's lexer/lexer.go as its specification. GoloScript's own lexer is | ||
| 41 | +// a Go package, but its module is named `golo` and is not importable from | ||
| 42 | +// another module, so the rules are carried here rather than called — and what | ||
| 43 | +// the lexer reads as one token, this scanner colours as one span. | ||
| 44 | +// | ||
| 45 | +// It is deliberately tolerant of broken input: source under the cursor is | ||
| 46 | +// invalid most of the time it is being typed, and a highlighter that gives up | ||
| 47 | +// is a highlighter that flickers off. | ||
| 48 | +// | ||
| 49 | +// spans := gololang.Highlight("function main = |args| {\n println(\"hi\")\n}\n") | ||
| 50 | +// // spans[0][0] covers "function" with syntax.ClassKeyword | ||
| 51 | +func Highlight(src string) [][]syntax.Span { | ||
| 52 | + return syntax.ScanLines(src, scanLine) | ||
| 53 | +} | ||
| 54 | + | ||
| 55 | +// scanLine colours one line, finishing whatever the previous line left open | ||
| 56 | +// before looking at anything new, and reports what this line leaves open. | ||
| 57 | +func scanLine(line []rune, open carry) ([]syntax.Span, carry) { | ||
| 58 | + s := syntax.NewLineScanner(line) | ||
| 59 | + | ||
| 60 | + open = finishOpen(s, open) | ||
| 61 | + for !s.AtEnd() { | ||
| 62 | + open = scanToken(s) | ||
| 63 | + } | ||
| 64 | + return s.Spans(), open | ||
| 65 | +} | ||
| 66 | + | ||
| 67 | +// finishOpen colours the continuation of a construct opened on an earlier | ||
| 68 | +// line, and says whether it is still open at the end of this one. With nothing | ||
| 69 | +// open it does nothing. | ||
| 70 | +func finishOpen(s *syntax.LineScanner, open carry) carry { | ||
| 71 | + switch open { | ||
| 72 | + case blockCommentOpen: | ||
| 73 | + return stillOpen(syntax.FinishBlockComment(s, blockCommentMarker, syntax.ClassComment), open) | ||
| 74 | + case multilineStringOpen: | ||
| 75 | + return stillOpen(syntax.FinishBlockComment(s, multilineStringQuote, syntax.ClassString), open) | ||
| 76 | + case stringOpen: | ||
| 77 | + return finishQuoted(s, '"', syntax.ClassString, open) | ||
| 78 | + case charOpen: | ||
| 79 | + return finishQuoted(s, '\'', syntax.ClassChar, open) | ||
| 80 | + default: | ||
| 81 | + return nothingOpen | ||
| 82 | + } | ||
| 83 | +} | ||
| 84 | + | ||
| 85 | +// stillOpen turns "did it close?" into what the next line should be told. | ||
| 86 | +func stillOpen(closed bool, open carry) carry { | ||
| 87 | + if closed { | ||
| 88 | + return nothingOpen | ||
| 89 | + } | ||
| 90 | + return open | ||
| 91 | +} | ||
| 92 | + | ||
| 93 | +// blockCommentMarker opens and closes a block comment. The lexer asks for four | ||
| 94 | +// dashes exactly: three are an operator run, and a fifth is part of the text. | ||
| 95 | +const blockCommentMarker = "----" | ||
| 96 | + | ||
| 97 | +// scanToken colours whatever starts at the scanner's position, and reports | ||
| 98 | +// which construct, if any, ran off the end of the line. | ||
| 99 | +// | ||
| 100 | +// The order of the cases is the design, and three of them are load-bearing: | ||
| 101 | +// | ||
| 102 | +// - ---- is tested before the operators, because - is an operator rune and | ||
| 103 | +// the run would otherwise be coloured as one. | ||
| 104 | +// - """ is tested before ", because both begin with a quote and what tells | ||
| 105 | +// them apart is the two runes after it. The lexer makes the same check in | ||
| 106 | +// the same order. | ||
| 107 | +// - A digit is tested before a word, and a word before a dot, so that 1.5 is | ||
| 108 | +// one number and hello.World is a name, a dot and a name. | ||
| 109 | +func scanToken(s *syntax.LineScanner) carry { | ||
| 110 | + r := s.Peek(0) | ||
| 111 | + | ||
| 112 | + switch { | ||
| 113 | + case r == ' ' || r == '\t': | ||
| 114 | + s.SkipSpaces() | ||
| 115 | + case r == '#': | ||
| 116 | + // # opens a comment that runs to the end of the line. A shebang is | ||
| 117 | + // one too: #!/usr/bin/env golo is how a script is run as a command, | ||
| 118 | + // and the interpreter reads it as a comment because that is what it | ||
| 119 | + // is. | ||
| 120 | + s.TakeRest(syntax.ClassComment) | ||
| 121 | + case s.HasPrefix(0, blockCommentMarker): | ||
| 122 | + return stillOpen(syntax.OpenBlockComment(s, blockCommentMarker, blockCommentMarker, syntax.ClassComment), blockCommentOpen) | ||
| 123 | + case s.HasPrefix(0, multilineStringQuote): | ||
| 124 | + return takeMultilineString(s) | ||
| 125 | + case r == '"': | ||
| 126 | + return takeQuoted(s, '"', syntax.ClassString, stringOpen) | ||
| 127 | + case r == '\'': | ||
| 128 | + return takeQuoted(s, '\'', syntax.ClassChar, charOpen) | ||
| 129 | + case syntax.IsDigit(r): | ||
| 130 | + takeNumber(s) | ||
| 131 | + case isIdentifierStart(r): | ||
| 132 | + takeWord(s) | ||
| 133 | + case r == '.': | ||
| 134 | + takeDot(s) | ||
| 135 | + case r == '$': | ||
| 136 | + // $ joins a union to one of its variants in `augment Shape$Circle`. | ||
| 137 | + // It is structure rather than computation, so it is punctuation like | ||
| 138 | + // the dot in a module path. | ||
| 139 | + s.Take(1, syntax.ClassPunctuation) | ||
| 140 | + case syntax.IsOperatorRune(r): | ||
| 141 | + s.TakeWhile(syntax.ClassOperator, syntax.IsOperatorRune) | ||
| 142 | + case syntax.IsPunctuationRune(r): | ||
| 143 | + s.Take(1, syntax.ClassPunctuation) | ||
| 144 | + default: | ||
| 145 | + // A rune nothing here claims — a stray control character, a symbol | ||
| 146 | + // outside the ranges the lexer accepts in a name — is stepped over | ||
| 147 | + // uncoloured rather than guessed at. | ||
| 148 | + s.Advance(1) | ||
| 149 | + } | ||
| 150 | + return nothingOpen | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +// takeDot colours a dot and whatever the language says belongs with it. | ||
| 154 | +// | ||
| 155 | +// A run of dots is an operator — .. is a range and ... marks a variadic | ||
| 156 | +// parameter — and a dot on its own is punctuation: the separator in a module | ||
| 157 | +// path such as hello.World, or in a decimal that takeNumber did not claim | ||
| 158 | +// because no digit came before it. | ||
| 159 | +func takeDot(s *syntax.LineScanner) { | ||
| 160 | + if s.Peek(1) == '.' { | ||
| 161 | + s.TakeWhile(syntax.ClassOperator, func(r rune) bool { return r == '.' }) | ||
| 162 | + return | ||
| 163 | + } | ||
| 164 | + s.Take(1, syntax.ClassPunctuation) | ||
| 165 | +} | ||
added
internal/gololang/scan_test.go +586 -0 | new file mode 100644 | ||
| @@ -0,0 +1,586 @@ | ||
| 1 | +package gololang_test | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "strings" | |
| 5 | + "testing" | |
| 6 | + | |
| 7 | + "rickub.com/turbo-editors/turbo-core/syntax" | |
| 8 | + | |
| 9 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | |
| 10 | +) | |
| 11 | + | |
| 12 | +// coloured is one span with the text it covers, which is what a test wants to | |
| 13 | +// talk about: "the word function is a keyword", not "columns 0 to 8 are class | |
| 14 | +// 1". | |
| 15 | +type coloured struct { | |
| 16 | + text string | |
| 17 | + class syntax.Class | |
| 18 | +} | |
| 19 | + | |
| 20 | +func (c coloured) String() string { return c.text + ":" + c.class.String() } | |
| 21 | + | |
| 22 | +// colouredLine returns every span of one line of source, with its text. | |
| 23 | +func colouredLine(t *testing.T, src string) []coloured { | |
| 24 | + t.Helper() | |
| 25 | + | |
| 26 | + lines := gololang.Highlight(src) | |
| 27 | + if len(lines) != 1 { | |
| 28 | + t.Fatalf("Highlight(%q) returned %d lines, want 1", src, len(lines)) | |
| 29 | + } | |
| 30 | + return withText([]rune(src), lines[0]) | |
| 31 | +} | |
| 32 | + | |
| 33 | +// withText pairs each span with the runes it covers. | |
| 34 | +func withText(line []rune, spans []syntax.Span) []coloured { | |
| 35 | + out := make([]coloured, 0, len(spans)) | |
| 36 | + for _, span := range spans { | |
| 37 | + out = append(out, coloured{string(line[span.Start:span.End]), span.Class}) | |
| 38 | + } | |
| 39 | + return out | |
| 40 | +} | |
| 41 | + | |
| 42 | +// find returns the span covering exactly the given text, if there is one. | |
| 43 | +func find(spans []coloured, text string) (coloured, bool) { | |
| 44 | + for _, span := range spans { | |
| 45 | + if span.text == text { | |
| 46 | + return span, true | |
| 47 | + } | |
| 48 | + } | |
| 49 | + return coloured{}, false | |
| 50 | +} | |
| 51 | + | |
| 52 | +// assertClass fails unless one span covers exactly text and has the wanted | |
| 53 | +// class. Asking for the whole text means a scanner that split a construct in | |
| 54 | +// two is caught, not only one that coloured it wrongly. | |
| 55 | +func assertClass(t *testing.T, src, text string, want syntax.Class) { | |
| 56 | + t.Helper() | |
| 57 | + | |
| 58 | + spans := colouredLine(t, src) | |
| 59 | + got, ok := find(spans, text) | |
| 60 | + if !ok { | |
| 61 | + t.Fatalf("in %q: no single span covers %q; got %v", src, text, spans) | |
| 62 | + } | |
| 63 | + if got.class != want { | |
| 64 | + t.Errorf("in %q: %q is %s, want %s", src, text, got.class, want) | |
| 65 | + } | |
| 66 | +} | |
| 67 | + | |
| 68 | +// lineOf returns the spans of one line of a multi-line document, with text. | |
| 69 | +func lineOf(src string, number int) []coloured { | |
| 70 | + lines := strings.Split(src, "\n") | |
| 71 | + return withText([]rune(lines[number]), gololang.Highlight(src)[number]) | |
| 72 | +} | |
| 73 | + | |
| 74 | +// --- the three invariants the editor relies on ------------------------------ | |
| 75 | + | |
| 76 | +// A representative body of Golo, used by the invariant tests below. It is | |
| 77 | +// deliberately a mixture: every construct the scanner knows, some broken | |
| 78 | +// input, and the constructs that most easily run into one another. | |
| 79 | +const sample = `#!/usr/bin/env golo | |
| 80 | +module demo.Shapes | |
| 81 | + | |
| 82 | +import gololang.Errors | |
| 83 | + | |
| 84 | +---- | |
| 85 | +A block comment, with --- near misses | |
| 86 | +and a "quote" inside it. | |
| 87 | +---- | |
| 88 | + | |
| 89 | +struct Point = { x, y } | |
| 90 | + | |
| 91 | +union Shape = { | |
| 92 | + Circle = { radius } | |
| 93 | + Rect = { width, height } | |
| 94 | +} | |
| 95 | + | |
| 96 | +augment Shape$Circle { | |
| 97 | + function area = |this| -> 3.14159 * this: radius() * this: radius() | |
| 98 | +} | |
| 99 | + | |
| 100 | +function main = |args| { | |
| 101 | + let p = Point(1, 2) | |
| 102 | + var big = 42L | |
| 103 | + let ratio = 2.5e-3F | |
| 104 | + let text = """ | |
| 105 | + a multi-line "string" | |
| 106 | + """ | |
| 107 | + let label = match { | |
| 108 | + when p: x() > 0 then "positive" | |
| 109 | + otherwise "other" | |
| 110 | + } | |
| 111 | + foreach i in range(0, 3) { | |
| 112 | + println("i = " + i + '\n') | |
| 113 | + } | |
| 114 | + let ok = p?: x() orIfNull 0 | |
| 115 | + let 😀 = list[1, 2, 3] | |
| 116 | + let broken = "unterminated | |
| 117 | + let after = 1 | |
| 118 | +}` | |
| 119 | + | |
| 120 | +func TestEveryLineGetsExactlyOneEntry(t *testing.T) { | |
| 121 | + // The editor indexes the result by line number without checking, so a | |
| 122 | + // scanner that returned one entry fewer would draw every line below the | |
| 123 | + // gap in the wrong colours. | |
| 124 | + src := sample + "\n\n\ntrailing\n" | |
| 125 | + want := len(strings.Split(src, "\n")) | |
| 126 | + | |
| 127 | + if got := len(gololang.Highlight(src)); got != want { | |
| 128 | + t.Errorf("Highlight returned %d lines for %d lines of source", got, want) | |
| 129 | + } | |
| 130 | +} | |
| 131 | + | |
| 132 | +func TestSpansAreInOrderAndDoNotOverlap(t *testing.T) { | |
| 133 | + // Spans are drawn in the order they arrive. Two out of order paint over | |
| 134 | + // each other, and nothing fails. | |
| 135 | + for number, spans := range gololang.Highlight(sample) { | |
| 136 | + line := []rune(strings.Split(sample, "\n")[number]) | |
| 137 | + previousEnd := 0 | |
| 138 | + | |
| 139 | + for _, span := range spans { | |
| 140 | + switch { | |
| 141 | + case span.Start < previousEnd: | |
| 142 | + t.Errorf("line %d: span %v starts before the previous one ended at %d", number+1, span, previousEnd) | |
| 143 | + case span.Start >= span.End: | |
| 144 | + t.Errorf("line %d: span %v is empty or inverted", number+1, span) | |
| 145 | + case span.End > len(line): | |
| 146 | + t.Errorf("line %d: span %v runs past the %d runes of the line", number+1, span, len(line)) | |
| 147 | + } | |
| 148 | + previousEnd = span.End | |
| 149 | + } | |
| 150 | + } | |
| 151 | +} | |
| 152 | + | |
| 153 | +func TestBrokenInputStillColours(t *testing.T) { | |
| 154 | + // Source under the cursor is invalid most of the time it is being typed. | |
| 155 | + broken := []string{ | |
| 156 | + `let x = "`, | |
| 157 | + `let x = '`, | |
| 158 | + `let x = "\`, | |
| 159 | + `"""`, | |
| 160 | + `""`, | |
| 161 | + `----`, | |
| 162 | + `---`, | |
| 163 | + `-----`, | |
| 164 | + `#`, | |
| 165 | + `function`, | |
| 166 | + `function (`, | |
| 167 | + `module`, | |
| 168 | + `import a.`, | |
| 169 | + `.`, | |
| 170 | + `..`, | |
| 171 | + `1.`, | |
| 172 | + `1e`, | |
| 173 | + `1e-`, | |
| 174 | + `$`, | |
| 175 | + `}}}`, | |
| 176 | + `|`, | |
| 177 | + `->`, | |
| 178 | + `?:`, | |
| 179 | + } | |
| 180 | + for _, src := range broken { | |
| 181 | + spans := gololang.Highlight(src) | |
| 182 | + if len(spans) != 1 { | |
| 183 | + t.Errorf("Highlight(%q) returned %d lines, want 1", src, len(spans)) | |
| 184 | + } | |
| 185 | + } | |
| 186 | +} | |
| 187 | + | |
| 188 | +func TestAnEmptyDocumentIsOneEmptyLine(t *testing.T) { | |
| 189 | + if got := gololang.Highlight(""); len(got) != 1 || len(got[0]) != 0 { | |
| 190 | + t.Errorf("Highlight(\"\") = %v, want one line with no spans", got) | |
| 191 | + } | |
| 192 | +} | |
| 193 | + | |
| 194 | +func TestCRLFColoursTheSameAsLF(t *testing.T) { | |
| 195 | + unix := gololang.Highlight("function main = |args| {\n println(\"hi\")\n}") | |
| 196 | + windows := gololang.Highlight("function main = |args| {\r\n println(\"hi\")\r\n}") | |
| 197 | + | |
| 198 | + if len(unix) != len(windows) { | |
| 199 | + t.Fatalf("CRLF gave %d lines, LF gave %d", len(windows), len(unix)) | |
| 200 | + } | |
| 201 | + for i := range unix { | |
| 202 | + if len(unix[i]) != len(windows[i]) { | |
| 203 | + t.Errorf("line %d: CRLF gave %v, LF gave %v", i+1, windows[i], unix[i]) | |
| 204 | + } | |
| 205 | + } | |
| 206 | +} | |
| 207 | + | |
| 208 | +// --- what crosses a line break ---------------------------------------------- | |
| 209 | + | |
| 210 | +func TestABlockCommentIsCarriedToItsClosingDashes(t *testing.T) { | |
| 211 | + src := "let a = 1 ----\nstill a comment\n---- let b = 2\nlet c = 3" | |
| 212 | + | |
| 213 | + if got, ok := find(lineOf(src, 1), "still a comment"); !ok || got.class != syntax.ClassComment { | |
| 214 | + t.Errorf("the line inside a block comment is %v, want all comment", lineOf(src, 1)) | |
| 215 | + } | |
| 216 | + third := lineOf(src, 2) | |
| 217 | + if got, ok := find(third, "----"); !ok || got.class != syntax.ClassComment { | |
| 218 | + t.Errorf("the closing dashes are %v, want a comment", third) | |
| 219 | + } | |
| 220 | + if got, ok := find(third, "let"); !ok || got.class != syntax.ClassKeyword { | |
| 221 | + t.Errorf("code after the closing dashes is %v, want a keyword", third) | |
| 222 | + } | |
| 223 | + if got, ok := find(lineOf(src, 3), "let"); !ok || got.class != syntax.ClassKeyword { | |
| 224 | + t.Errorf("the line after the comment is %v, want code", lineOf(src, 3)) | |
| 225 | + } | |
| 226 | +} | |
| 227 | + | |
| 228 | +func TestThreeDashesDoNotCloseABlockComment(t *testing.T) { | |
| 229 | + // The lexer asks for four dashes. Three inside a comment are its text, and | |
| 230 | + // the tree-sitter grammar's own test — "with --- near misses" — is the | |
| 231 | + // same case. | |
| 232 | + src := "----\nwith --- near misses\nlet x = 1 ----\nlet y = 2" | |
| 233 | + | |
| 234 | + if got, ok := find(lineOf(src, 2), "let x = 1 ----"); !ok || got.class != syntax.ClassComment { | |
| 235 | + t.Errorf("a line inside the comment after a near miss is %v, want all comment", lineOf(src, 2)) | |
| 236 | + } | |
| 237 | + if got, ok := find(lineOf(src, 3), "let"); !ok || got.class != syntax.ClassKeyword { | |
| 238 | + t.Errorf("the line after the comment is %v, want code", lineOf(src, 3)) | |
| 239 | + } | |
| 240 | +} | |
| 241 | + | |
| 242 | +func TestAStringIsCarriedToItsClosingQuote(t *testing.T) { | |
| 243 | + // The interpreter reads a string to its closing quote and stops at | |
| 244 | + // nothing in between, so the colour follows it. This is the decision the | |
| 245 | + // other scanners in the family make the other way, for languages whose | |
| 246 | + // grammar forbids the newline; Golo's lexer does not. | |
| 247 | + src := "let s = \"first line\nsecond line\nthird\" + rest\nlet next = 1" | |
| 248 | + | |
| 249 | + if got, ok := find(lineOf(src, 1), "second line"); !ok || got.class != syntax.ClassString { | |
| 250 | + t.Errorf("the middle of a multi-line string is %v, want all string", lineOf(src, 1)) | |
| 251 | + } | |
| 252 | + third := lineOf(src, 2) | |
| 253 | + if got, ok := find(third, `third"`); !ok || got.class != syntax.ClassString { | |
| 254 | + t.Errorf("the end of the string is %v, want string up to the quote", third) | |
| 255 | + } | |
| 256 | + if got, ok := find(third, "rest"); !ok || got.class != syntax.ClassIdentifier { | |
| 257 | + t.Errorf("code after the closing quote is %v, want a name", third) | |
| 258 | + } | |
| 259 | + if got, ok := find(lineOf(src, 3), "let"); !ok || got.class != syntax.ClassKeyword { | |
| 260 | + t.Errorf("the line after the string is %v, want code", lineOf(src, 3)) | |
| 261 | + } | |
| 262 | +} | |
| 263 | + | |
| 264 | +func TestATripleQuotedStringIsCarriedToItsClosingQuotes(t *testing.T) { | |
| 265 | + src := "let s = \"\"\"\na \"quoted\" line # not a comment\n\"\"\" + rest\nlet next = 1" | |
| 266 | + | |
| 267 | + second := lineOf(src, 1) | |
| 268 | + if len(second) != 1 || second[0].class != syntax.ClassString { | |
| 269 | + t.Errorf("a line inside a triple-quoted string is %v, want one string span", second) | |
| 270 | + } | |
| 271 | + third := lineOf(src, 2) | |
| 272 | + if got, ok := find(third, `"""`); !ok || got.class != syntax.ClassString { | |
| 273 | + t.Errorf("the closing quotes are %v, want string", third) | |
| 274 | + } | |
| 275 | + if got, ok := find(third, "rest"); !ok || got.class != syntax.ClassIdentifier { | |
| 276 | + t.Errorf("code after the closing quotes is %v, want a name", third) | |
| 277 | + } | |
| 278 | +} | |
| 279 | + | |
| 280 | +func TestACharacterLiteralIsCarriedLikeAString(t *testing.T) { | |
| 281 | + // The same loop in lexer.go reads both, so a stray apostrophe paints to | |
| 282 | + // the next apostrophe, wherever that is. | |
| 283 | + src := "let c = 'x\nstill' + 1" | |
| 284 | + | |
| 285 | + if got, ok := find(lineOf(src, 1), "still'"); !ok || got.class != syntax.ClassChar { | |
| 286 | + t.Errorf("the continuation of a character literal is %v, want char", lineOf(src, 1)) | |
| 287 | + } | |
| 288 | + if got, ok := find(lineOf(src, 1), "1"); !ok || got.class != syntax.ClassNumber { | |
| 289 | + t.Errorf("code after the closing apostrophe is %v, want a number", lineOf(src, 1)) | |
| 290 | + } | |
| 291 | +} | |
| 292 | + | |
| 293 | +func TestAnEscapedQuoteAtTheEndOfALineKeepsTheStringOpen(t *testing.T) { | |
| 294 | + // A backslash before the newline escapes it, and the lexer keeps reading. | |
| 295 | + src := "let s = \"ends with a slash \\\nand goes on\"\nlet next = 1" | |
| 296 | + | |
| 297 | + if got, ok := find(lineOf(src, 1), `and goes on"`); !ok || got.class != syntax.ClassString { | |
| 298 | + t.Errorf("after an escaped newline the string is %v, want string", lineOf(src, 1)) | |
| 299 | + } | |
| 300 | +} | |
| 301 | + | |
| 302 | +func TestALineCommentEndsAtTheLine(t *testing.T) { | |
| 303 | + src := "# a comment\nlet x = 1" | |
| 304 | + | |
| 305 | + if got, ok := find(lineOf(src, 1), "let"); !ok || got.class != syntax.ClassKeyword { | |
| 306 | + t.Errorf("the line after a # comment is %v, want code", lineOf(src, 1)) | |
| 307 | + } | |
| 308 | +} | |
| 309 | + | |
| 310 | +// --- one case per construct ------------------------------------------------- | |
| 311 | + | |
| 312 | +func TestConstructs(t *testing.T) { | |
| 313 | + cases := []struct { | |
| 314 | + name string | |
| 315 | + src string | |
| 316 | + text string | |
| 317 | + class syntax.Class | |
| 318 | + }{ | |
| 319 | + {"line comment", `let x = 1 # why`, `# why`, syntax.ClassComment}, | |
| 320 | + {"shebang", `#!/usr/bin/env golo`, `#!/usr/bin/env golo`, syntax.ClassComment}, | |
| 321 | + {"block comment on one line", `let x = 1 ---- why ---- + 2`, `---- why ----`, syntax.ClassComment}, | |
| 322 | + {"code after a one-line block comment", `let x = 1 ---- why ---- + 2`, `2`, syntax.ClassNumber}, | |
| 323 | + {"empty block comment", `--------`, `--------`, syntax.ClassComment}, | |
| 324 | + {"hash inside a string is not a comment", `let s = "# not a comment"`, `"# not a comment"`, syntax.ClassString}, | |
| 325 | + {"dashes inside a string are not a comment", `let s = "---- not a comment ----"`, `"---- not a comment ----"`, syntax.ClassString}, | |
| 326 | + | |
| 327 | + {"string", `let s = "hi"`, `"hi"`, syntax.ClassString}, | |
| 328 | + {"string stops at its closing quote", `let s = "hi" + name`, `"hi"`, syntax.ClassString}, | |
| 329 | + {"code after a string is still code", `let s = "hi" + name`, `name`, syntax.ClassIdentifier}, | |
| 330 | + {"empty string", `let s = ""`, `""`, syntax.ClassString}, | |
| 331 | + {"string with an escaped quote", `let s = "he said \"hi\""`, `"he said \"hi\""`, syntax.ClassString}, | |
| 332 | + {"string with a hex escape", `let s = "\x41"`, `"\x41"`, syntax.ClassString}, | |
| 333 | + {"triple-quoted string on one line", `let s = """a "b" c""" + d`, `"""a "b" c"""`, syntax.ClassString}, | |
| 334 | + {"code after a triple-quoted string", `let s = """a "b" c""" + d`, `d`, syntax.ClassIdentifier}, | |
| 335 | + {"char literal", `let c = 'x'`, `'x'`, syntax.ClassChar}, | |
| 336 | + {"escaped char literal", `let c = '\n'`, `'\n'`, syntax.ClassChar}, | |
| 337 | + {"char stops at its closing quote", `let c = 'x' + 1`, `1`, syntax.ClassNumber}, | |
| 338 | + | |
| 339 | + {"integer", `let n = 42`, `42`, syntax.ClassNumber}, | |
| 340 | + {"long", `let n = 42L`, `42L`, syntax.ClassNumber}, | |
| 341 | + {"double", `let n = 3.14`, `3.14`, syntax.ClassNumber}, | |
| 342 | + {"float with a capital suffix", `let n = 3.14F`, `3.14F`, syntax.ClassNumber}, | |
| 343 | + {"float with a small suffix", `let n = 2.0f`, `2.0f`, syntax.ClassNumber}, | |
| 344 | + {"exponent", `let n = 1.5e3`, `1.5e3`, syntax.ClassNumber}, | |
| 345 | + {"negative exponent", `let n = 1.5e-3`, `1.5e-3`, syntax.ClassNumber}, | |
| 346 | + {"integer with an exponent", `let n = 2E10`, `2E10`, syntax.ClassNumber}, | |
| 347 | + {"minus is an operator, not part of the number", `let n = -1`, `-`, syntax.ClassOperator}, | |
| 348 | + | |
| 349 | + {"keyword", `function main = |args| {`, `function`, syntax.ClassKeyword}, | |
| 350 | + {"local keyword", `local function helper = |x| -> x`, `local`, syntax.ClassKeyword}, | |
| 351 | + {"word operator", `let ok = a and b`, `and`, syntax.ClassKeyword}, | |
| 352 | + {"orIfNull", `let v = x orIfNull 0`, `orIfNull`, syntax.ClassKeyword}, | |
| 353 | + {"oftype", `if x oftype String.class {`, `oftype`, syntax.ClassKeyword}, | |
| 354 | + {"match", `let l = match {`, `match`, syntax.ClassKeyword}, | |
| 355 | + {"when then otherwise", ` when x then "y"`, `then`, syntax.ClassKeyword}, | |
| 356 | + {"constant true", `let ok = true`, `true`, syntax.ClassConstant}, | |
| 357 | + {"constant null", `let n = null`, `null`, syntax.ClassConstant}, | |
| 358 | + {"builtin", `println("hi")`, `println`, syntax.ClassBuiltin}, | |
| 359 | + {"builtin collection literal", `let xs = list[1, 2]`, `list`, syntax.ClassBuiltin}, | |
| 360 | + {"builtin spelt like a type", `let o = DynamicObject()`, `DynamicObject`, syntax.ClassBuiltin}, | |
| 361 | + {"builtin range", `foreach i in range(0, 3) {`, `range`, syntax.ClassBuiltin}, | |
| 362 | + | |
| 363 | + {"declared function", `function main = |args| {`, `main`, syntax.ClassFunction}, | |
| 364 | + {"declared function with an arrow body", `function twice = |x| -> x * 2`, `twice`, syntax.ClassFunction}, | |
| 365 | + {"declared emoji function", `function 🚀launch = {`, `🚀launch`, syntax.ClassFunction}, | |
| 366 | + {"call", `helper(1)`, `helper`, syntax.ClassFunction}, | |
| 367 | + {"method call after a colon", `this: radius()`, `radius`, syntax.ClassFunction}, | |
| 368 | + {"plain identifier", `let shape = other`, `other`, syntax.ClassIdentifier}, | |
| 369 | + {"emoji identifier", `let 😀 = 1`, `😀`, syntax.ClassIdentifier}, | |
| 370 | + {"accented identifier", `let été = 1`, `été`, syntax.ClassIdentifier}, | |
| 371 | + {"CJK identifier", `let 名前 = 1`, `名前`, syntax.ClassIdentifier}, | |
| 372 | + {"underscore identifier", `let _hidden = 1`, `_hidden`, syntax.ClassIdentifier}, | |
| 373 | + | |
| 374 | + {"struct name", `struct Point = { x, y }`, `Point`, syntax.ClassType}, | |
| 375 | + {"union name", `union Shape = {`, `Shape`, syntax.ClassType}, | |
| 376 | + {"variant", ` Circle = { radius }`, `Circle`, syntax.ClassType}, | |
| 377 | + {"constructor call", `let p = Point(1, 2)`, `Point`, syntax.ClassType}, | |
| 378 | + {"variant constructor", `let r = Result_Failure("no")`, `Result_Failure`, syntax.ClassType}, | |
| 379 | + {"augment target", `augment Person {`, `Person`, syntax.ClassType}, | |
| 380 | + {"union variant separator", `augment Shape$Circle {`, `$`, syntax.ClassPunctuation}, | |
| 381 | + | |
| 382 | + {"module path", `module hello.World`, `hello.World`, syntax.ClassType}, | |
| 383 | + {"import path", `import gololang.Errors`, `gololang.Errors`, syntax.ClassType}, | |
| 384 | + {"three-part import path", `import java.util.List`, `java.util.List`, syntax.ClassType}, | |
| 385 | + | |
| 386 | + {"closure bars", `let f = |x| -> x`, `|`, syntax.ClassOperator}, | |
| 387 | + {"arrow", `let f = |x| -> x`, `->`, syntax.ClassOperator}, | |
| 388 | + {"colon", `this: name()`, `:`, syntax.ClassOperator}, | |
| 389 | + {"safe navigation", `let n = p?: x()`, `?:`, syntax.ClassOperator}, | |
| 390 | + {"comparison", `if a <= b {`, `<=`, syntax.ClassOperator}, | |
| 391 | + {"not equal", `if a != b {`, `!=`, syntax.ClassOperator}, | |
| 392 | + {"range operator", `let r = 1..3`, `..`, syntax.ClassOperator}, | |
| 393 | + {"range does not swallow the number", `let r = 1..3`, `1`, syntax.ClassNumber}, | |
| 394 | + {"variadic dots", `function f = |args...| {`, `...`, syntax.ClassOperator}, | |
| 395 | + {"module dot outside a path", `let x = a.b`, `.`, syntax.ClassPunctuation}, | |
| 396 | + {"brace", `function main = |args| {`, `{`, syntax.ClassPunctuation}, | |
| 397 | + {"bracket", `let xs = list[1]`, `[`, syntax.ClassPunctuation}, | |
| 398 | + {"comma", `struct Point = { x, y }`, `,`, syntax.ClassPunctuation}, | |
| 399 | + } | |
| 400 | + | |
| 401 | + for _, c := range cases { | |
| 402 | + t.Run(c.name, func(t *testing.T) { | |
| 403 | + assertClass(t, c.src, c.text, c.class) | |
| 404 | + }) | |
| 405 | + } | |
| 406 | +} | |
| 407 | + | |
| 408 | +// --- one case per thing the scanner deliberately refuses -------------------- | |
| 409 | + | |
| 410 | +func TestRefusals(t *testing.T) { | |
| 411 | + cases := []struct { | |
| 412 | + name string | |
| 413 | + why string | |
| 414 | + src string | |
| 415 | + text string | |
| 416 | + class syntax.Class | |
| 417 | + }{ | |
| 418 | + { | |
| 419 | + name: "no digit separators", | |
| 420 | + why: "the lexer has none, so 1_000 is the number 1 followed by the name _000", | |
| 421 | + src: `let n = 1_000`, | |
| 422 | + text: `1`, | |
| 423 | + class: syntax.ClassNumber, | |
| 424 | + }, | |
| 425 | + { | |
| 426 | + name: "no hexadecimal", | |
| 427 | + why: "the lexer has none, so 0xFF is the number 0 followed by the name xFF", | |
| 428 | + src: `let n = 0xFF`, | |
| 429 | + text: `xFF`, | |
| 430 | + class: syntax.ClassIdentifier, | |
| 431 | + }, | |
| 432 | + { | |
| 433 | + name: "a leading dot is never a number", | |
| 434 | + why: "the lexer requires a digit before the point, so .5 is a dot and then a number", | |
| 435 | + src: `let n = .5`, | |
| 436 | + text: `.`, | |
| 437 | + class: syntax.ClassPunctuation, | |
| 438 | + }, | |
| 439 | + { | |
| 440 | + name: "a lower-case l is not a long suffix", | |
| 441 | + why: "the lexer accepts only the upper-case L, so 42l is 42 and then the name l", | |
| 442 | + src: `let n = 42l`, | |
| 443 | + text: `42`, | |
| 444 | + class: syntax.ClassNumber, | |
| 445 | + }, | |
| 446 | + { | |
| 447 | + name: "three dashes are an operator run", | |
| 448 | + why: "the lexer asks for four dashes to open a comment; three are two minus signs and a third", | |
| 449 | + src: `let x = a --- b`, | |
| 450 | + text: `---`, | |
| 451 | + class: syntax.ClassOperator, | |
| 452 | + }, | |
| 453 | + { | |
| 454 | + name: "a constructor of your own is a type", | |
| 455 | + why: "nothing in the syntax separates Circle(1.0) from a type applied to arguments", | |
| 456 | + src: `let c = Circle(1.0)`, | |
| 457 | + text: `Circle`, | |
| 458 | + class: syntax.ClassType, | |
| 459 | + }, | |
| 460 | + { | |
| 461 | + name: "Some is not a constant", | |
| 462 | + why: "in Golo it is a variant of an ordinary union declared in gololang.Errors, not a builtin", | |
| 463 | + src: `let s = Some(1)`, | |
| 464 | + text: `Some`, | |
| 465 | + class: syntax.ClassType, | |
| 466 | + }, | |
| 467 | + { | |
| 468 | + name: "a capitalised variable is a type", | |
| 469 | + why: "the case rule is a convention, and the scanner follows the convention rather than the parser", | |
| 470 | + src: `let Count = 1`, | |
| 471 | + text: `Count`, | |
| 472 | + class: syntax.ClassType, | |
| 473 | + }, | |
| 474 | + { | |
| 475 | + name: "a keyword used as a method name stays a keyword", | |
| 476 | + why: "the scanner does not track what a colon introduces, and the lexer would refuse the word anyway", | |
| 477 | + src: `obj: match()`, | |
| 478 | + text: `match`, | |
| 479 | + class: syntax.ClassKeyword, | |
| 480 | + }, | |
| 481 | + { | |
| 482 | + name: "a module path stops at a dot with nothing after it", | |
| 483 | + why: "half-typed `import a.` leaves the dot as punctuation rather than swallowing it", | |
| 484 | + src: `import a.`, | |
| 485 | + text: `.`, | |
| 486 | + class: syntax.ClassPunctuation, | |
| 487 | + }, | |
| 488 | + { | |
| 489 | + name: "a string nothing closes runs to the end of the line and beyond", | |
| 490 | + why: "the interpreter reads to the closing quote wherever it is, so the colour follows it", | |
| 491 | + src: `let s = "oops`, | |
| 492 | + text: `"oops`, | |
| 493 | + class: syntax.ClassString, | |
| 494 | + }, | |
| 495 | + { | |
| 496 | + name: "no escapes inside a triple-quoted string", | |
| 497 | + why: "the lexer appends every rune until the three quotes, so a backslash-quote does not protect them", | |
| 498 | + src: `let s = """a\""" + b`, | |
| 499 | + text: `"""a\"""`, | |
| 500 | + class: syntax.ClassString, | |
| 501 | + }, | |
| 502 | + } | |
| 503 | + | |
| 504 | + for _, c := range cases { | |
| 505 | + t.Run(c.name, func(t *testing.T) { | |
| 506 | + assertClass(t, c.src, c.text, c.class) | |
| 507 | + }) | |
| 508 | + } | |
| 509 | +} | |
| 510 | + | |
| 511 | +func TestAnUnterminatedStringPaintsTheNextLine(t *testing.T) { | |
| 512 | + // The other half of the carry decision, stated as what a user sees: the | |
| 513 | + // line after a stray quote is coloured as string, because that is what | |
| 514 | + // the interpreter will read it as. | |
| 515 | + src := "let broken = \"unterminated\nlet after = 1" | |
| 516 | + | |
| 517 | + if got, ok := find(lineOf(src, 1), "let after = 1"); !ok || got.class != syntax.ClassString { | |
| 518 | + t.Errorf("the line after an unterminated string is %v, want all string", lineOf(src, 1)) | |
| 519 | + } | |
| 520 | +} | |
| 521 | + | |
| 522 | +func TestTheDeclaredNameAfterFunctionIsAFunctionEvenWhenNoParenthesisFollows(t *testing.T) { | |
| 523 | + // Everywhere else a name is a function because a parenthesis follows it. | |
| 524 | + // A declaration is followed by an equals sign, and is the one place a | |
| 525 | + // reader most wants the colour. | |
| 526 | + spans := colouredLine(t, `function main = |args| {`) | |
| 527 | + | |
| 528 | + got, ok := find(spans, "main") | |
| 529 | + if !ok || got.class != syntax.ClassFunction { | |
| 530 | + t.Errorf("the declared name is %v, want a function; got %v", got, spans) | |
| 531 | + } | |
| 532 | + if got, ok := find(spans, "args"); !ok || got.class != syntax.ClassIdentifier { | |
| 533 | + t.Errorf("the parameter is %v, want a plain name", got) | |
| 534 | + } | |
| 535 | +} | |
| 536 | + | |
| 537 | +func TestTheKeywordTableIsEveryReservedWordButTheLiterals(t *testing.T) { | |
| 538 | + // token/token.go in GoloScript reserves 41 words. Three of them are the | |
| 539 | + // literal values, which are constants here; the other 38 are keywords. | |
| 540 | + keywords := gololang.Keywords() | |
| 541 | + | |
| 542 | + if len(keywords) != 38 { | |
| 543 | + t.Errorf("the scanner knows %d keywords, want 38", len(keywords)) | |
| 544 | + } | |
| 545 | + for _, literal := range []string{"true", "false", "null"} { | |
| 546 | + for _, keyword := range keywords { | |
| 547 | + if keyword == literal { | |
| 548 | + t.Errorf("%q is in the keyword table; it is a constant", literal) | |
| 549 | + } | |
| 550 | + } | |
| 551 | + } | |
| 552 | +} | |
| 553 | + | |
| 554 | +func TestTheBuiltinTableHoldsWhatTheInterpreterProvides(t *testing.T) { | |
| 555 | + // evaluator.BuiltinNames() answers 162 names, five of which begin with a | |
| 556 | + // double underscore and are the test runner's own counters. A test in | |
| 557 | + // editor_test.go holds this table to a real golo when one is installed; | |
| 558 | + // this one holds its shape when none is. | |
| 559 | + builtins := gololang.Builtins() | |
| 560 | + | |
| 561 | + if len(builtins) != 157 { | |
| 562 | + t.Errorf("the scanner knows %d builtins, want 157", len(builtins)) | |
| 563 | + } | |
| 564 | + seen := map[string]bool{} | |
| 565 | + for _, name := range builtins { | |
| 566 | + if strings.HasPrefix(name, "__") { | |
| 567 | + t.Errorf("%q is an internal helper and should not be coloured as a builtin", name) | |
| 568 | + } | |
| 569 | + if seen[name] { | |
| 570 | + t.Errorf("%q is listed twice", name) | |
| 571 | + } | |
| 572 | + seen[name] = true | |
| 573 | + } | |
| 574 | +} | |
| 575 | + | |
| 576 | +func TestHighlightIsWhatTheRegistryUses(t *testing.T) { | |
| 577 | + gololang.Register() | |
| 578 | + | |
| 579 | + spans := syntax.Highlight(gololang.Language, "function main = |args| {\n") | |
| 580 | + if len(spans) == 0 || len(spans[0]) == 0 { | |
| 581 | + t.Fatalf("syntax.Highlight gave nothing for Golo: %v", spans) | |
| 582 | + } | |
| 583 | + if spans[0][0].Class != syntax.ClassKeyword { | |
| 584 | + t.Errorf("the registered highlighter coloured function as %s, want a keyword", spans[0][0].Class) | |
| 585 | + } | |
| 586 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,586 @@ | |||
| 1 | +package gololang_test | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "strings" | ||
| 5 | + "testing" | ||
| 6 | + | ||
| 7 | + "rickub.com/turbo-editors/turbo-core/syntax" | ||
| 8 | + | ||
| 9 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | ||
| 10 | +) | ||
| 11 | + | ||
| 12 | +// coloured is one span with the text it covers, which is what a test wants to | ||
| 13 | +// talk about: "the word function is a keyword", not "columns 0 to 8 are class | ||
| 14 | +// 1". | ||
| 15 | +type coloured struct { | ||
| 16 | + text string | ||
| 17 | + class syntax.Class | ||
| 18 | +} | ||
| 19 | + | ||
| 20 | +func (c coloured) String() string { return c.text + ":" + c.class.String() } | ||
| 21 | + | ||
| 22 | +// colouredLine returns every span of one line of source, with its text. | ||
| 23 | +func colouredLine(t *testing.T, src string) []coloured { | ||
| 24 | + t.Helper() | ||
| 25 | + | ||
| 26 | + lines := gololang.Highlight(src) | ||
| 27 | + if len(lines) != 1 { | ||
| 28 | + t.Fatalf("Highlight(%q) returned %d lines, want 1", src, len(lines)) | ||
| 29 | + } | ||
| 30 | + return withText([]rune(src), lines[0]) | ||
| 31 | +} | ||
| 32 | + | ||
| 33 | +// withText pairs each span with the runes it covers. | ||
| 34 | +func withText(line []rune, spans []syntax.Span) []coloured { | ||
| 35 | + out := make([]coloured, 0, len(spans)) | ||
| 36 | + for _, span := range spans { | ||
| 37 | + out = append(out, coloured{string(line[span.Start:span.End]), span.Class}) | ||
| 38 | + } | ||
| 39 | + return out | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +// find returns the span covering exactly the given text, if there is one. | ||
| 43 | +func find(spans []coloured, text string) (coloured, bool) { | ||
| 44 | + for _, span := range spans { | ||
| 45 | + if span.text == text { | ||
| 46 | + return span, true | ||
| 47 | + } | ||
| 48 | + } | ||
| 49 | + return coloured{}, false | ||
| 50 | +} | ||
| 51 | + | ||
| 52 | +// assertClass fails unless one span covers exactly text and has the wanted | ||
| 53 | +// class. Asking for the whole text means a scanner that split a construct in | ||
| 54 | +// two is caught, not only one that coloured it wrongly. | ||
| 55 | +func assertClass(t *testing.T, src, text string, want syntax.Class) { | ||
| 56 | + t.Helper() | ||
| 57 | + | ||
| 58 | + spans := colouredLine(t, src) | ||
| 59 | + got, ok := find(spans, text) | ||
| 60 | + if !ok { | ||
| 61 | + t.Fatalf("in %q: no single span covers %q; got %v", src, text, spans) | ||
| 62 | + } | ||
| 63 | + if got.class != want { | ||
| 64 | + t.Errorf("in %q: %q is %s, want %s", src, text, got.class, want) | ||
| 65 | + } | ||
| 66 | +} | ||
| 67 | + | ||
| 68 | +// lineOf returns the spans of one line of a multi-line document, with text. | ||
| 69 | +func lineOf(src string, number int) []coloured { | ||
| 70 | + lines := strings.Split(src, "\n") | ||
| 71 | + return withText([]rune(lines[number]), gololang.Highlight(src)[number]) | ||
| 72 | +} | ||
| 73 | + | ||
| 74 | +// --- the three invariants the editor relies on ------------------------------ | ||
| 75 | + | ||
| 76 | +// A representative body of Golo, used by the invariant tests below. It is | ||
| 77 | +// deliberately a mixture: every construct the scanner knows, some broken | ||
| 78 | +// input, and the constructs that most easily run into one another. | ||
| 79 | +const sample = `#!/usr/bin/env golo | ||
| 80 | +module demo.Shapes | ||
| 81 | + | ||
| 82 | +import gololang.Errors | ||
| 83 | + | ||
| 84 | +---- | ||
| 85 | +A block comment, with --- near misses | ||
| 86 | +and a "quote" inside it. | ||
| 87 | +---- | ||
| 88 | + | ||
| 89 | +struct Point = { x, y } | ||
| 90 | + | ||
| 91 | +union Shape = { | ||
| 92 | + Circle = { radius } | ||
| 93 | + Rect = { width, height } | ||
| 94 | +} | ||
| 95 | + | ||
| 96 | +augment Shape$Circle { | ||
| 97 | + function area = |this| -> 3.14159 * this: radius() * this: radius() | ||
| 98 | +} | ||
| 99 | + | ||
| 100 | +function main = |args| { | ||
| 101 | + let p = Point(1, 2) | ||
| 102 | + var big = 42L | ||
| 103 | + let ratio = 2.5e-3F | ||
| 104 | + let text = """ | ||
| 105 | + a multi-line "string" | ||
| 106 | + """ | ||
| 107 | + let label = match { | ||
| 108 | + when p: x() > 0 then "positive" | ||
| 109 | + otherwise "other" | ||
| 110 | + } | ||
| 111 | + foreach i in range(0, 3) { | ||
| 112 | + println("i = " + i + '\n') | ||
| 113 | + } | ||
| 114 | + let ok = p?: x() orIfNull 0 | ||
| 115 | + let 😀 = list[1, 2, 3] | ||
| 116 | + let broken = "unterminated | ||
| 117 | + let after = 1 | ||
| 118 | +}` | ||
| 119 | + | ||
| 120 | +func TestEveryLineGetsExactlyOneEntry(t *testing.T) { | ||
| 121 | + // The editor indexes the result by line number without checking, so a | ||
| 122 | + // scanner that returned one entry fewer would draw every line below the | ||
| 123 | + // gap in the wrong colours. | ||
| 124 | + src := sample + "\n\n\ntrailing\n" | ||
| 125 | + want := len(strings.Split(src, "\n")) | ||
| 126 | + | ||
| 127 | + if got := len(gololang.Highlight(src)); got != want { | ||
| 128 | + t.Errorf("Highlight returned %d lines for %d lines of source", got, want) | ||
| 129 | + } | ||
| 130 | +} | ||
| 131 | + | ||
| 132 | +func TestSpansAreInOrderAndDoNotOverlap(t *testing.T) { | ||
| 133 | + // Spans are drawn in the order they arrive. Two out of order paint over | ||
| 134 | + // each other, and nothing fails. | ||
| 135 | + for number, spans := range gololang.Highlight(sample) { | ||
| 136 | + line := []rune(strings.Split(sample, "\n")[number]) | ||
| 137 | + previousEnd := 0 | ||
| 138 | + | ||
| 139 | + for _, span := range spans { | ||
| 140 | + switch { | ||
| 141 | + case span.Start < previousEnd: | ||
| 142 | + t.Errorf("line %d: span %v starts before the previous one ended at %d", number+1, span, previousEnd) | ||
| 143 | + case span.Start >= span.End: | ||
| 144 | + t.Errorf("line %d: span %v is empty or inverted", number+1, span) | ||
| 145 | + case span.End > len(line): | ||
| 146 | + t.Errorf("line %d: span %v runs past the %d runes of the line", number+1, span, len(line)) | ||
| 147 | + } | ||
| 148 | + previousEnd = span.End | ||
| 149 | + } | ||
| 150 | + } | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +func TestBrokenInputStillColours(t *testing.T) { | ||
| 154 | + // Source under the cursor is invalid most of the time it is being typed. | ||
| 155 | + broken := []string{ | ||
| 156 | + `let x = "`, | ||
| 157 | + `let x = '`, | ||
| 158 | + `let x = "\`, | ||
| 159 | + `"""`, | ||
| 160 | + `""`, | ||
| 161 | + `----`, | ||
| 162 | + `---`, | ||
| 163 | + `-----`, | ||
| 164 | + `#`, | ||
| 165 | + `function`, | ||
| 166 | + `function (`, | ||
| 167 | + `module`, | ||
| 168 | + `import a.`, | ||
| 169 | + `.`, | ||
| 170 | + `..`, | ||
| 171 | + `1.`, | ||
| 172 | + `1e`, | ||
| 173 | + `1e-`, | ||
| 174 | + `$`, | ||
| 175 | + `}}}`, | ||
| 176 | + `|`, | ||
| 177 | + `->`, | ||
| 178 | + `?:`, | ||
| 179 | + } | ||
| 180 | + for _, src := range broken { | ||
| 181 | + spans := gololang.Highlight(src) | ||
| 182 | + if len(spans) != 1 { | ||
| 183 | + t.Errorf("Highlight(%q) returned %d lines, want 1", src, len(spans)) | ||
| 184 | + } | ||
| 185 | + } | ||
| 186 | +} | ||
| 187 | + | ||
| 188 | +func TestAnEmptyDocumentIsOneEmptyLine(t *testing.T) { | ||
| 189 | + if got := gololang.Highlight(""); len(got) != 1 || len(got[0]) != 0 { | ||
| 190 | + t.Errorf("Highlight(\"\") = %v, want one line with no spans", got) | ||
| 191 | + } | ||
| 192 | +} | ||
| 193 | + | ||
| 194 | +func TestCRLFColoursTheSameAsLF(t *testing.T) { | ||
| 195 | + unix := gololang.Highlight("function main = |args| {\n println(\"hi\")\n}") | ||
| 196 | + windows := gololang.Highlight("function main = |args| {\r\n println(\"hi\")\r\n}") | ||
| 197 | + | ||
| 198 | + if len(unix) != len(windows) { | ||
| 199 | + t.Fatalf("CRLF gave %d lines, LF gave %d", len(windows), len(unix)) | ||
| 200 | + } | ||
| 201 | + for i := range unix { | ||
| 202 | + if len(unix[i]) != len(windows[i]) { | ||
| 203 | + t.Errorf("line %d: CRLF gave %v, LF gave %v", i+1, windows[i], unix[i]) | ||
| 204 | + } | ||
| 205 | + } | ||
| 206 | +} | ||
| 207 | + | ||
| 208 | +// --- what crosses a line break ---------------------------------------------- | ||
| 209 | + | ||
| 210 | +func TestABlockCommentIsCarriedToItsClosingDashes(t *testing.T) { | ||
| 211 | + src := "let a = 1 ----\nstill a comment\n---- let b = 2\nlet c = 3" | ||
| 212 | + | ||
| 213 | + if got, ok := find(lineOf(src, 1), "still a comment"); !ok || got.class != syntax.ClassComment { | ||
| 214 | + t.Errorf("the line inside a block comment is %v, want all comment", lineOf(src, 1)) | ||
| 215 | + } | ||
| 216 | + third := lineOf(src, 2) | ||
| 217 | + if got, ok := find(third, "----"); !ok || got.class != syntax.ClassComment { | ||
| 218 | + t.Errorf("the closing dashes are %v, want a comment", third) | ||
| 219 | + } | ||
| 220 | + if got, ok := find(third, "let"); !ok || got.class != syntax.ClassKeyword { | ||
| 221 | + t.Errorf("code after the closing dashes is %v, want a keyword", third) | ||
| 222 | + } | ||
| 223 | + if got, ok := find(lineOf(src, 3), "let"); !ok || got.class != syntax.ClassKeyword { | ||
| 224 | + t.Errorf("the line after the comment is %v, want code", lineOf(src, 3)) | ||
| 225 | + } | ||
| 226 | +} | ||
| 227 | + | ||
| 228 | +func TestThreeDashesDoNotCloseABlockComment(t *testing.T) { | ||
| 229 | + // The lexer asks for four dashes. Three inside a comment are its text, and | ||
| 230 | + // the tree-sitter grammar's own test — "with --- near misses" — is the | ||
| 231 | + // same case. | ||
| 232 | + src := "----\nwith --- near misses\nlet x = 1 ----\nlet y = 2" | ||
| 233 | + | ||
| 234 | + if got, ok := find(lineOf(src, 2), "let x = 1 ----"); !ok || got.class != syntax.ClassComment { | ||
| 235 | + t.Errorf("a line inside the comment after a near miss is %v, want all comment", lineOf(src, 2)) | ||
| 236 | + } | ||
| 237 | + if got, ok := find(lineOf(src, 3), "let"); !ok || got.class != syntax.ClassKeyword { | ||
| 238 | + t.Errorf("the line after the comment is %v, want code", lineOf(src, 3)) | ||
| 239 | + } | ||
| 240 | +} | ||
| 241 | + | ||
| 242 | +func TestAStringIsCarriedToItsClosingQuote(t *testing.T) { | ||
| 243 | + // The interpreter reads a string to its closing quote and stops at | ||
| 244 | + // nothing in between, so the colour follows it. This is the decision the | ||
| 245 | + // other scanners in the family make the other way, for languages whose | ||
| 246 | + // grammar forbids the newline; Golo's lexer does not. | ||
| 247 | + src := "let s = \"first line\nsecond line\nthird\" + rest\nlet next = 1" | ||
| 248 | + | ||
| 249 | + if got, ok := find(lineOf(src, 1), "second line"); !ok || got.class != syntax.ClassString { | ||
| 250 | + t.Errorf("the middle of a multi-line string is %v, want all string", lineOf(src, 1)) | ||
| 251 | + } | ||
| 252 | + third := lineOf(src, 2) | ||
| 253 | + if got, ok := find(third, `third"`); !ok || got.class != syntax.ClassString { | ||
| 254 | + t.Errorf("the end of the string is %v, want string up to the quote", third) | ||
| 255 | + } | ||
| 256 | + if got, ok := find(third, "rest"); !ok || got.class != syntax.ClassIdentifier { | ||
| 257 | + t.Errorf("code after the closing quote is %v, want a name", third) | ||
| 258 | + } | ||
| 259 | + if got, ok := find(lineOf(src, 3), "let"); !ok || got.class != syntax.ClassKeyword { | ||
| 260 | + t.Errorf("the line after the string is %v, want code", lineOf(src, 3)) | ||
| 261 | + } | ||
| 262 | +} | ||
| 263 | + | ||
| 264 | +func TestATripleQuotedStringIsCarriedToItsClosingQuotes(t *testing.T) { | ||
| 265 | + src := "let s = \"\"\"\na \"quoted\" line # not a comment\n\"\"\" + rest\nlet next = 1" | ||
| 266 | + | ||
| 267 | + second := lineOf(src, 1) | ||
| 268 | + if len(second) != 1 || second[0].class != syntax.ClassString { | ||
| 269 | + t.Errorf("a line inside a triple-quoted string is %v, want one string span", second) | ||
| 270 | + } | ||
| 271 | + third := lineOf(src, 2) | ||
| 272 | + if got, ok := find(third, `"""`); !ok || got.class != syntax.ClassString { | ||
| 273 | + t.Errorf("the closing quotes are %v, want string", third) | ||
| 274 | + } | ||
| 275 | + if got, ok := find(third, "rest"); !ok || got.class != syntax.ClassIdentifier { | ||
| 276 | + t.Errorf("code after the closing quotes is %v, want a name", third) | ||
| 277 | + } | ||
| 278 | +} | ||
| 279 | + | ||
| 280 | +func TestACharacterLiteralIsCarriedLikeAString(t *testing.T) { | ||
| 281 | + // The same loop in lexer.go reads both, so a stray apostrophe paints to | ||
| 282 | + // the next apostrophe, wherever that is. | ||
| 283 | + src := "let c = 'x\nstill' + 1" | ||
| 284 | + | ||
| 285 | + if got, ok := find(lineOf(src, 1), "still'"); !ok || got.class != syntax.ClassChar { | ||
| 286 | + t.Errorf("the continuation of a character literal is %v, want char", lineOf(src, 1)) | ||
| 287 | + } | ||
| 288 | + if got, ok := find(lineOf(src, 1), "1"); !ok || got.class != syntax.ClassNumber { | ||
| 289 | + t.Errorf("code after the closing apostrophe is %v, want a number", lineOf(src, 1)) | ||
| 290 | + } | ||
| 291 | +} | ||
| 292 | + | ||
| 293 | +func TestAnEscapedQuoteAtTheEndOfALineKeepsTheStringOpen(t *testing.T) { | ||
| 294 | + // A backslash before the newline escapes it, and the lexer keeps reading. | ||
| 295 | + src := "let s = \"ends with a slash \\\nand goes on\"\nlet next = 1" | ||
| 296 | + | ||
| 297 | + if got, ok := find(lineOf(src, 1), `and goes on"`); !ok || got.class != syntax.ClassString { | ||
| 298 | + t.Errorf("after an escaped newline the string is %v, want string", lineOf(src, 1)) | ||
| 299 | + } | ||
| 300 | +} | ||
| 301 | + | ||
| 302 | +func TestALineCommentEndsAtTheLine(t *testing.T) { | ||
| 303 | + src := "# a comment\nlet x = 1" | ||
| 304 | + | ||
| 305 | + if got, ok := find(lineOf(src, 1), "let"); !ok || got.class != syntax.ClassKeyword { | ||
| 306 | + t.Errorf("the line after a # comment is %v, want code", lineOf(src, 1)) | ||
| 307 | + } | ||
| 308 | +} | ||
| 309 | + | ||
| 310 | +// --- one case per construct ------------------------------------------------- | ||
| 311 | + | ||
| 312 | +func TestConstructs(t *testing.T) { | ||
| 313 | + cases := []struct { | ||
| 314 | + name string | ||
| 315 | + src string | ||
| 316 | + text string | ||
| 317 | + class syntax.Class | ||
| 318 | + }{ | ||
| 319 | + {"line comment", `let x = 1 # why`, `# why`, syntax.ClassComment}, | ||
| 320 | + {"shebang", `#!/usr/bin/env golo`, `#!/usr/bin/env golo`, syntax.ClassComment}, | ||
| 321 | + {"block comment on one line", `let x = 1 ---- why ---- + 2`, `---- why ----`, syntax.ClassComment}, | ||
| 322 | + {"code after a one-line block comment", `let x = 1 ---- why ---- + 2`, `2`, syntax.ClassNumber}, | ||
| 323 | + {"empty block comment", `--------`, `--------`, syntax.ClassComment}, | ||
| 324 | + {"hash inside a string is not a comment", `let s = "# not a comment"`, `"# not a comment"`, syntax.ClassString}, | ||
| 325 | + {"dashes inside a string are not a comment", `let s = "---- not a comment ----"`, `"---- not a comment ----"`, syntax.ClassString}, | ||
| 326 | + | ||
| 327 | + {"string", `let s = "hi"`, `"hi"`, syntax.ClassString}, | ||
| 328 | + {"string stops at its closing quote", `let s = "hi" + name`, `"hi"`, syntax.ClassString}, | ||
| 329 | + {"code after a string is still code", `let s = "hi" + name`, `name`, syntax.ClassIdentifier}, | ||
| 330 | + {"empty string", `let s = ""`, `""`, syntax.ClassString}, | ||
| 331 | + {"string with an escaped quote", `let s = "he said \"hi\""`, `"he said \"hi\""`, syntax.ClassString}, | ||
| 332 | + {"string with a hex escape", `let s = "\x41"`, `"\x41"`, syntax.ClassString}, | ||
| 333 | + {"triple-quoted string on one line", `let s = """a "b" c""" + d`, `"""a "b" c"""`, syntax.ClassString}, | ||
| 334 | + {"code after a triple-quoted string", `let s = """a "b" c""" + d`, `d`, syntax.ClassIdentifier}, | ||
| 335 | + {"char literal", `let c = 'x'`, `'x'`, syntax.ClassChar}, | ||
| 336 | + {"escaped char literal", `let c = '\n'`, `'\n'`, syntax.ClassChar}, | ||
| 337 | + {"char stops at its closing quote", `let c = 'x' + 1`, `1`, syntax.ClassNumber}, | ||
| 338 | + | ||
| 339 | + {"integer", `let n = 42`, `42`, syntax.ClassNumber}, | ||
| 340 | + {"long", `let n = 42L`, `42L`, syntax.ClassNumber}, | ||
| 341 | + {"double", `let n = 3.14`, `3.14`, syntax.ClassNumber}, | ||
| 342 | + {"float with a capital suffix", `let n = 3.14F`, `3.14F`, syntax.ClassNumber}, | ||
| 343 | + {"float with a small suffix", `let n = 2.0f`, `2.0f`, syntax.ClassNumber}, | ||
| 344 | + {"exponent", `let n = 1.5e3`, `1.5e3`, syntax.ClassNumber}, | ||
| 345 | + {"negative exponent", `let n = 1.5e-3`, `1.5e-3`, syntax.ClassNumber}, | ||
| 346 | + {"integer with an exponent", `let n = 2E10`, `2E10`, syntax.ClassNumber}, | ||
| 347 | + {"minus is an operator, not part of the number", `let n = -1`, `-`, syntax.ClassOperator}, | ||
| 348 | + | ||
| 349 | + {"keyword", `function main = |args| {`, `function`, syntax.ClassKeyword}, | ||
| 350 | + {"local keyword", `local function helper = |x| -> x`, `local`, syntax.ClassKeyword}, | ||
| 351 | + {"word operator", `let ok = a and b`, `and`, syntax.ClassKeyword}, | ||
| 352 | + {"orIfNull", `let v = x orIfNull 0`, `orIfNull`, syntax.ClassKeyword}, | ||
| 353 | + {"oftype", `if x oftype String.class {`, `oftype`, syntax.ClassKeyword}, | ||
| 354 | + {"match", `let l = match {`, `match`, syntax.ClassKeyword}, | ||
| 355 | + {"when then otherwise", ` when x then "y"`, `then`, syntax.ClassKeyword}, | ||
| 356 | + {"constant true", `let ok = true`, `true`, syntax.ClassConstant}, | ||
| 357 | + {"constant null", `let n = null`, `null`, syntax.ClassConstant}, | ||
| 358 | + {"builtin", `println("hi")`, `println`, syntax.ClassBuiltin}, | ||
| 359 | + {"builtin collection literal", `let xs = list[1, 2]`, `list`, syntax.ClassBuiltin}, | ||
| 360 | + {"builtin spelt like a type", `let o = DynamicObject()`, `DynamicObject`, syntax.ClassBuiltin}, | ||
| 361 | + {"builtin range", `foreach i in range(0, 3) {`, `range`, syntax.ClassBuiltin}, | ||
| 362 | + | ||
| 363 | + {"declared function", `function main = |args| {`, `main`, syntax.ClassFunction}, | ||
| 364 | + {"declared function with an arrow body", `function twice = |x| -> x * 2`, `twice`, syntax.ClassFunction}, | ||
| 365 | + {"declared emoji function", `function 🚀launch = {`, `🚀launch`, syntax.ClassFunction}, | ||
| 366 | + {"call", `helper(1)`, `helper`, syntax.ClassFunction}, | ||
| 367 | + {"method call after a colon", `this: radius()`, `radius`, syntax.ClassFunction}, | ||
| 368 | + {"plain identifier", `let shape = other`, `other`, syntax.ClassIdentifier}, | ||
| 369 | + {"emoji identifier", `let 😀 = 1`, `😀`, syntax.ClassIdentifier}, | ||
| 370 | + {"accented identifier", `let été = 1`, `été`, syntax.ClassIdentifier}, | ||
| 371 | + {"CJK identifier", `let 名前 = 1`, `名前`, syntax.ClassIdentifier}, | ||
| 372 | + {"underscore identifier", `let _hidden = 1`, `_hidden`, syntax.ClassIdentifier}, | ||
| 373 | + | ||
| 374 | + {"struct name", `struct Point = { x, y }`, `Point`, syntax.ClassType}, | ||
| 375 | + {"union name", `union Shape = {`, `Shape`, syntax.ClassType}, | ||
| 376 | + {"variant", ` Circle = { radius }`, `Circle`, syntax.ClassType}, | ||
| 377 | + {"constructor call", `let p = Point(1, 2)`, `Point`, syntax.ClassType}, | ||
| 378 | + {"variant constructor", `let r = Result_Failure("no")`, `Result_Failure`, syntax.ClassType}, | ||
| 379 | + {"augment target", `augment Person {`, `Person`, syntax.ClassType}, | ||
| 380 | + {"union variant separator", `augment Shape$Circle {`, `$`, syntax.ClassPunctuation}, | ||
| 381 | + | ||
| 382 | + {"module path", `module hello.World`, `hello.World`, syntax.ClassType}, | ||
| 383 | + {"import path", `import gololang.Errors`, `gololang.Errors`, syntax.ClassType}, | ||
| 384 | + {"three-part import path", `import java.util.List`, `java.util.List`, syntax.ClassType}, | ||
| 385 | + | ||
| 386 | + {"closure bars", `let f = |x| -> x`, `|`, syntax.ClassOperator}, | ||
| 387 | + {"arrow", `let f = |x| -> x`, `->`, syntax.ClassOperator}, | ||
| 388 | + {"colon", `this: name()`, `:`, syntax.ClassOperator}, | ||
| 389 | + {"safe navigation", `let n = p?: x()`, `?:`, syntax.ClassOperator}, | ||
| 390 | + {"comparison", `if a <= b {`, `<=`, syntax.ClassOperator}, | ||
| 391 | + {"not equal", `if a != b {`, `!=`, syntax.ClassOperator}, | ||
| 392 | + {"range operator", `let r = 1..3`, `..`, syntax.ClassOperator}, | ||
| 393 | + {"range does not swallow the number", `let r = 1..3`, `1`, syntax.ClassNumber}, | ||
| 394 | + {"variadic dots", `function f = |args...| {`, `...`, syntax.ClassOperator}, | ||
| 395 | + {"module dot outside a path", `let x = a.b`, `.`, syntax.ClassPunctuation}, | ||
| 396 | + {"brace", `function main = |args| {`, `{`, syntax.ClassPunctuation}, | ||
| 397 | + {"bracket", `let xs = list[1]`, `[`, syntax.ClassPunctuation}, | ||
| 398 | + {"comma", `struct Point = { x, y }`, `,`, syntax.ClassPunctuation}, | ||
| 399 | + } | ||
| 400 | + | ||
| 401 | + for _, c := range cases { | ||
| 402 | + t.Run(c.name, func(t *testing.T) { | ||
| 403 | + assertClass(t, c.src, c.text, c.class) | ||
| 404 | + }) | ||
| 405 | + } | ||
| 406 | +} | ||
| 407 | + | ||
| 408 | +// --- one case per thing the scanner deliberately refuses -------------------- | ||
| 409 | + | ||
| 410 | +func TestRefusals(t *testing.T) { | ||
| 411 | + cases := []struct { | ||
| 412 | + name string | ||
| 413 | + why string | ||
| 414 | + src string | ||
| 415 | + text string | ||
| 416 | + class syntax.Class | ||
| 417 | + }{ | ||
| 418 | + { | ||
| 419 | + name: "no digit separators", | ||
| 420 | + why: "the lexer has none, so 1_000 is the number 1 followed by the name _000", | ||
| 421 | + src: `let n = 1_000`, | ||
| 422 | + text: `1`, | ||
| 423 | + class: syntax.ClassNumber, | ||
| 424 | + }, | ||
| 425 | + { | ||
| 426 | + name: "no hexadecimal", | ||
| 427 | + why: "the lexer has none, so 0xFF is the number 0 followed by the name xFF", | ||
| 428 | + src: `let n = 0xFF`, | ||
| 429 | + text: `xFF`, | ||
| 430 | + class: syntax.ClassIdentifier, | ||
| 431 | + }, | ||
| 432 | + { | ||
| 433 | + name: "a leading dot is never a number", | ||
| 434 | + why: "the lexer requires a digit before the point, so .5 is a dot and then a number", | ||
| 435 | + src: `let n = .5`, | ||
| 436 | + text: `.`, | ||
| 437 | + class: syntax.ClassPunctuation, | ||
| 438 | + }, | ||
| 439 | + { | ||
| 440 | + name: "a lower-case l is not a long suffix", | ||
| 441 | + why: "the lexer accepts only the upper-case L, so 42l is 42 and then the name l", | ||
| 442 | + src: `let n = 42l`, | ||
| 443 | + text: `42`, | ||
| 444 | + class: syntax.ClassNumber, | ||
| 445 | + }, | ||
| 446 | + { | ||
| 447 | + name: "three dashes are an operator run", | ||
| 448 | + why: "the lexer asks for four dashes to open a comment; three are two minus signs and a third", | ||
| 449 | + src: `let x = a --- b`, | ||
| 450 | + text: `---`, | ||
| 451 | + class: syntax.ClassOperator, | ||
| 452 | + }, | ||
| 453 | + { | ||
| 454 | + name: "a constructor of your own is a type", | ||
| 455 | + why: "nothing in the syntax separates Circle(1.0) from a type applied to arguments", | ||
| 456 | + src: `let c = Circle(1.0)`, | ||
| 457 | + text: `Circle`, | ||
| 458 | + class: syntax.ClassType, | ||
| 459 | + }, | ||
| 460 | + { | ||
| 461 | + name: "Some is not a constant", | ||
| 462 | + why: "in Golo it is a variant of an ordinary union declared in gololang.Errors, not a builtin", | ||
| 463 | + src: `let s = Some(1)`, | ||
| 464 | + text: `Some`, | ||
| 465 | + class: syntax.ClassType, | ||
| 466 | + }, | ||
| 467 | + { | ||
| 468 | + name: "a capitalised variable is a type", | ||
| 469 | + why: "the case rule is a convention, and the scanner follows the convention rather than the parser", | ||
| 470 | + src: `let Count = 1`, | ||
| 471 | + text: `Count`, | ||
| 472 | + class: syntax.ClassType, | ||
| 473 | + }, | ||
| 474 | + { | ||
| 475 | + name: "a keyword used as a method name stays a keyword", | ||
| 476 | + why: "the scanner does not track what a colon introduces, and the lexer would refuse the word anyway", | ||
| 477 | + src: `obj: match()`, | ||
| 478 | + text: `match`, | ||
| 479 | + class: syntax.ClassKeyword, | ||
| 480 | + }, | ||
| 481 | + { | ||
| 482 | + name: "a module path stops at a dot with nothing after it", | ||
| 483 | + why: "half-typed `import a.` leaves the dot as punctuation rather than swallowing it", | ||
| 484 | + src: `import a.`, | ||
| 485 | + text: `.`, | ||
| 486 | + class: syntax.ClassPunctuation, | ||
| 487 | + }, | ||
| 488 | + { | ||
| 489 | + name: "a string nothing closes runs to the end of the line and beyond", | ||
| 490 | + why: "the interpreter reads to the closing quote wherever it is, so the colour follows it", | ||
| 491 | + src: `let s = "oops`, | ||
| 492 | + text: `"oops`, | ||
| 493 | + class: syntax.ClassString, | ||
| 494 | + }, | ||
| 495 | + { | ||
| 496 | + name: "no escapes inside a triple-quoted string", | ||
| 497 | + why: "the lexer appends every rune until the three quotes, so a backslash-quote does not protect them", | ||
| 498 | + src: `let s = """a\""" + b`, | ||
| 499 | + text: `"""a\"""`, | ||
| 500 | + class: syntax.ClassString, | ||
| 501 | + }, | ||
| 502 | + } | ||
| 503 | + | ||
| 504 | + for _, c := range cases { | ||
| 505 | + t.Run(c.name, func(t *testing.T) { | ||
| 506 | + assertClass(t, c.src, c.text, c.class) | ||
| 507 | + }) | ||
| 508 | + } | ||
| 509 | +} | ||
| 510 | + | ||
| 511 | +func TestAnUnterminatedStringPaintsTheNextLine(t *testing.T) { | ||
| 512 | + // The other half of the carry decision, stated as what a user sees: the | ||
| 513 | + // line after a stray quote is coloured as string, because that is what | ||
| 514 | + // the interpreter will read it as. | ||
| 515 | + src := "let broken = \"unterminated\nlet after = 1" | ||
| 516 | + | ||
| 517 | + if got, ok := find(lineOf(src, 1), "let after = 1"); !ok || got.class != syntax.ClassString { | ||
| 518 | + t.Errorf("the line after an unterminated string is %v, want all string", lineOf(src, 1)) | ||
| 519 | + } | ||
| 520 | +} | ||
| 521 | + | ||
| 522 | +func TestTheDeclaredNameAfterFunctionIsAFunctionEvenWhenNoParenthesisFollows(t *testing.T) { | ||
| 523 | + // Everywhere else a name is a function because a parenthesis follows it. | ||
| 524 | + // A declaration is followed by an equals sign, and is the one place a | ||
| 525 | + // reader most wants the colour. | ||
| 526 | + spans := colouredLine(t, `function main = |args| {`) | ||
| 527 | + | ||
| 528 | + got, ok := find(spans, "main") | ||
| 529 | + if !ok || got.class != syntax.ClassFunction { | ||
| 530 | + t.Errorf("the declared name is %v, want a function; got %v", got, spans) | ||
| 531 | + } | ||
| 532 | + if got, ok := find(spans, "args"); !ok || got.class != syntax.ClassIdentifier { | ||
| 533 | + t.Errorf("the parameter is %v, want a plain name", got) | ||
| 534 | + } | ||
| 535 | +} | ||
| 536 | + | ||
| 537 | +func TestTheKeywordTableIsEveryReservedWordButTheLiterals(t *testing.T) { | ||
| 538 | + // token/token.go in GoloScript reserves 41 words. Three of them are the | ||
| 539 | + // literal values, which are constants here; the other 38 are keywords. | ||
| 540 | + keywords := gololang.Keywords() | ||
| 541 | + | ||
| 542 | + if len(keywords) != 38 { | ||
| 543 | + t.Errorf("the scanner knows %d keywords, want 38", len(keywords)) | ||
| 544 | + } | ||
| 545 | + for _, literal := range []string{"true", "false", "null"} { | ||
| 546 | + for _, keyword := range keywords { | ||
| 547 | + if keyword == literal { | ||
| 548 | + t.Errorf("%q is in the keyword table; it is a constant", literal) | ||
| 549 | + } | ||
| 550 | + } | ||
| 551 | + } | ||
| 552 | +} | ||
| 553 | + | ||
| 554 | +func TestTheBuiltinTableHoldsWhatTheInterpreterProvides(t *testing.T) { | ||
| 555 | + // evaluator.BuiltinNames() answers 162 names, five of which begin with a | ||
| 556 | + // double underscore and are the test runner's own counters. A test in | ||
| 557 | + // editor_test.go holds this table to a real golo when one is installed; | ||
| 558 | + // this one holds its shape when none is. | ||
| 559 | + builtins := gololang.Builtins() | ||
| 560 | + | ||
| 561 | + if len(builtins) != 157 { | ||
| 562 | + t.Errorf("the scanner knows %d builtins, want 157", len(builtins)) | ||
| 563 | + } | ||
| 564 | + seen := map[string]bool{} | ||
| 565 | + for _, name := range builtins { | ||
| 566 | + if strings.HasPrefix(name, "__") { | ||
| 567 | + t.Errorf("%q is an internal helper and should not be coloured as a builtin", name) | ||
| 568 | + } | ||
| 569 | + if seen[name] { | ||
| 570 | + t.Errorf("%q is listed twice", name) | ||
| 571 | + } | ||
| 572 | + seen[name] = true | ||
| 573 | + } | ||
| 574 | +} | ||
| 575 | + | ||
| 576 | +func TestHighlightIsWhatTheRegistryUses(t *testing.T) { | ||
| 577 | + gololang.Register() | ||
| 578 | + | ||
| 579 | + spans := syntax.Highlight(gololang.Language, "function main = |args| {\n") | ||
| 580 | + if len(spans) == 0 || len(spans[0]) == 0 { | ||
| 581 | + t.Fatalf("syntax.Highlight gave nothing for Golo: %v", spans) | ||
| 582 | + } | ||
| 583 | + if spans[0][0].Class != syntax.ClassKeyword { | ||
| 584 | + t.Errorf("the registered highlighter coloured function as %s, want a keyword", spans[0][0].Class) | ||
| 585 | + } | ||
| 586 | +} | ||
added
internal/gololang/settings.toml.tmpl +18 -0 | new file mode 100644 | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +# turbo-golo project settings. | |
| 2 | +# | |
| 3 | +# These apply to everyone who opens this project in turbo-golo. Delete this | |
| 4 | +# file and the editor falls back to its own defaults. | |
| 5 | + | |
| 6 | +[editor] | |
| 7 | + | |
| 8 | +# The colour theme to start in. `turbo-golo -list-themes` lists them all. | |
| 9 | +# A -theme flag on the command line overrides this. | |
| 10 | +theme = %q | |
| 11 | + | |
| 12 | +# Write modified files by themselves, a short while after you stop typing. | |
| 13 | +# On, because a project that has gone to the trouble of having a settings file | |
| 14 | +# has said what it wants; set it to false and save, and it stops at once. | |
| 15 | +autosave = true | |
| 16 | + | |
| 17 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | |
| 18 | +autosave_delay = %q | |
| new file mode 100644 | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | +# turbo-golo project settings. | ||
| 2 | +# | ||
| 3 | +# These apply to everyone who opens this project in turbo-golo. Delete this | ||
| 4 | +# file and the editor falls back to its own defaults. | ||
| 5 | + | ||
| 6 | +[editor] | ||
| 7 | + | ||
| 8 | +# The colour theme to start in. `turbo-golo -list-themes` lists them all. | ||
| 9 | +# A -theme flag on the command line overrides this. | ||
| 10 | +theme = %q | ||
| 11 | + | ||
| 12 | +# Write modified files by themselves, a short while after you stop typing. | ||
| 13 | +# On, because a project that has gone to the trouble of having a settings file | ||
| 14 | +# has said what it wants; set it to false and save, and it stops at once. | ||
| 15 | +autosave = true | ||
| 16 | + | ||
| 17 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | ||
| 18 | +autosave_delay = %q | ||
added
internal/gololang/snippets.toml.tmpl +145 -0 | new file mode 100644 | ||
| @@ -0,0 +1,145 @@ | ||
| 1 | +# turbo-golo snippets. | |
| 2 | +# | |
| 3 | +# Each [[snippet]] becomes one line of the Snippets menu. Snippets sharing a | |
| 4 | +# group appear together in a submenu of that name; one with no group goes into | |
| 5 | +# %s. A snippet is inserted at the cursor, and every line after the | |
| 6 | +# first is indented to match the line you inserted it on. | |
| 7 | +# | |
| 8 | +# languages restricts a snippet to files of those kinds, by the names the | |
| 9 | +# editor uses: bash, dockerfile, golo, html, javascript, markdown, toml, xml, | |
| 10 | +# yaml. Leave it out and the snippet is offered everywhere. | |
| 11 | +# | |
| 12 | +# Bodies are indented with two spaces, which is what every example in the | |
| 13 | +# GoloScript documentation and its own templates use. Golo has no formatter to | |
| 14 | +# disagree with, so the convention is the only authority there is. | |
| 15 | +# | |
| 16 | +# Every Golo body below is written in single quotes — '''…''' rather than | |
| 17 | +# """…""" — because a Golo string carries \n and \" the way a Go string does, | |
| 18 | +# and TOML would interpret those escapes in a basic string before the editor | |
| 19 | +# ever saw them. In a literal string a backslash is just a backslash, which is | |
| 20 | +# what a Golo snippet needs. | |
| 21 | +# | |
| 22 | +# Your own snippets, shared across every project, go in: | |
| 23 | +# %s | |
| 24 | + | |
| 25 | +[[snippet]] | |
| 26 | +name = "module" | |
| 27 | +group = "Golo" | |
| 28 | +languages = ["golo"] | |
| 29 | +body = ''' | |
| 30 | +module hello.World | |
| 31 | + | |
| 32 | +function main = |args| { | |
| 33 | + println("Hello, Golo!") | |
| 34 | +}''' | |
| 35 | + | |
| 36 | +[[snippet]] | |
| 37 | +name = "main" | |
| 38 | +group = "Golo" | |
| 39 | +languages = ["golo"] | |
| 40 | +body = ''' | |
| 41 | +function main = |args| { | |
| 42 | + println("Hello, Golo!") | |
| 43 | +}''' | |
| 44 | + | |
| 45 | +[[snippet]] | |
| 46 | +name = "function" | |
| 47 | +group = "Golo" | |
| 48 | +languages = ["golo"] | |
| 49 | +body = ''' | |
| 50 | +function name = |a, b| { | |
| 51 | + return a + b | |
| 52 | +}''' | |
| 53 | + | |
| 54 | +[[snippet]] | |
| 55 | +name = "closure" | |
| 56 | +group = "Golo" | |
| 57 | +languages = ["golo"] | |
| 58 | +body = ''' | |
| 59 | +let f = |x| -> x * 2''' | |
| 60 | + | |
| 61 | +[[snippet]] | |
| 62 | +name = "struct" | |
| 63 | +group = "Golo" | |
| 64 | +languages = ["golo"] | |
| 65 | +body = ''' | |
| 66 | +struct Point = { x, y }''' | |
| 67 | + | |
| 68 | +[[snippet]] | |
| 69 | +name = "union" | |
| 70 | +group = "Golo" | |
| 71 | +languages = ["golo"] | |
| 72 | +body = ''' | |
| 73 | +union Shape = { | |
| 74 | + Circle = { radius } | |
| 75 | + Rect = { width, height } | |
| 76 | +}''' | |
| 77 | + | |
| 78 | +[[snippet]] | |
| 79 | +name = "augment" | |
| 80 | +group = "Golo" | |
| 81 | +languages = ["golo"] | |
| 82 | +body = ''' | |
| 83 | +augment Point { | |
| 84 | + function describe = |this| -> "(" + this: x() + ", " + this: y() + ")" | |
| 85 | +}''' | |
| 86 | + | |
| 87 | +[[snippet]] | |
| 88 | +name = "match" | |
| 89 | +group = "Golo" | |
| 90 | +languages = ["golo"] | |
| 91 | +body = ''' | |
| 92 | +let label = match { | |
| 93 | + when n < 0 then "negative" | |
| 94 | + when n == 0 then "zero" | |
| 95 | + otherwise "positive" | |
| 96 | +}''' | |
| 97 | + | |
| 98 | +[[snippet]] | |
| 99 | +name = "foreach" | |
| 100 | +group = "Golo" | |
| 101 | +languages = ["golo"] | |
| 102 | +body = ''' | |
| 103 | +foreach item in list[1, 2, 3] { | |
| 104 | + println(item) | |
| 105 | +}''' | |
| 106 | + | |
| 107 | +[[snippet]] | |
| 108 | +name = "for" | |
| 109 | +group = "Golo" | |
| 110 | +languages = ["golo"] | |
| 111 | +body = ''' | |
| 112 | +for (var i = 0, i < 10, i = i + 1) { | |
| 113 | + println(i) | |
| 114 | +}''' | |
| 115 | + | |
| 116 | +[[snippet]] | |
| 117 | +name = "try" | |
| 118 | +group = "Golo" | |
| 119 | +languages = ["golo"] | |
| 120 | +body = ''' | |
| 121 | +try { | |
| 122 | + throw "boom" | |
| 123 | +} catch (e) { | |
| 124 | + println("caught: \"" + e + "\"") | |
| 125 | +} finally { | |
| 126 | + println("done") | |
| 127 | +}''' | |
| 128 | + | |
| 129 | +[[snippet]] | |
| 130 | +name = "comprehension" | |
| 131 | +group = "Golo" | |
| 132 | +languages = ["golo"] | |
| 133 | +body = ''' | |
| 134 | +let squares = list[x * x foreach x in range(1, 6) when x > 2]''' | |
| 135 | + | |
| 136 | +[[snippet]] | |
| 137 | +group = "General" | |
| 138 | +name = "Hello" | |
| 139 | +body = "Hello!!!" | |
| 140 | + | |
| 141 | +[[snippet]] | |
| 142 | +group = "Markdown" | |
| 143 | +name = "Image" | |
| 144 | +languages = ["markdown"] | |
| 145 | +body = "" | |
| new file mode 100644 | |||
| @@ -0,0 +1,145 @@ | |||
| 1 | +# turbo-golo snippets. | ||
| 2 | +# | ||
| 3 | +# Each [[snippet]] becomes one line of the Snippets menu. Snippets sharing a | ||
| 4 | +# group appear together in a submenu of that name; one with no group goes into | ||
| 5 | +# %s. A snippet is inserted at the cursor, and every line after the | ||
| 6 | +# first is indented to match the line you inserted it on. | ||
| 7 | +# | ||
| 8 | +# languages restricts a snippet to files of those kinds, by the names the | ||
| 9 | +# editor uses: bash, dockerfile, golo, html, javascript, markdown, toml, xml, | ||
| 10 | +# yaml. Leave it out and the snippet is offered everywhere. | ||
| 11 | +# | ||
| 12 | +# Bodies are indented with two spaces, which is what every example in the | ||
| 13 | +# GoloScript documentation and its own templates use. Golo has no formatter to | ||
| 14 | +# disagree with, so the convention is the only authority there is. | ||
| 15 | +# | ||
| 16 | +# Every Golo body below is written in single quotes — '''…''' rather than | ||
| 17 | +# """…""" — because a Golo string carries \n and \" the way a Go string does, | ||
| 18 | +# and TOML would interpret those escapes in a basic string before the editor | ||
| 19 | +# ever saw them. In a literal string a backslash is just a backslash, which is | ||
| 20 | +# what a Golo snippet needs. | ||
| 21 | +# | ||
| 22 | +# Your own snippets, shared across every project, go in: | ||
| 23 | +# %s | ||
| 24 | + | ||
| 25 | +[[snippet]] | ||
| 26 | +name = "module" | ||
| 27 | +group = "Golo" | ||
| 28 | +languages = ["golo"] | ||
| 29 | +body = ''' | ||
| 30 | +module hello.World | ||
| 31 | + | ||
| 32 | +function main = |args| { | ||
| 33 | + println("Hello, Golo!") | ||
| 34 | +}''' | ||
| 35 | + | ||
| 36 | +[[snippet]] | ||
| 37 | +name = "main" | ||
| 38 | +group = "Golo" | ||
| 39 | +languages = ["golo"] | ||
| 40 | +body = ''' | ||
| 41 | +function main = |args| { | ||
| 42 | + println("Hello, Golo!") | ||
| 43 | +}''' | ||
| 44 | + | ||
| 45 | +[[snippet]] | ||
| 46 | +name = "function" | ||
| 47 | +group = "Golo" | ||
| 48 | +languages = ["golo"] | ||
| 49 | +body = ''' | ||
| 50 | +function name = |a, b| { | ||
| 51 | + return a + b | ||
| 52 | +}''' | ||
| 53 | + | ||
| 54 | +[[snippet]] | ||
| 55 | +name = "closure" | ||
| 56 | +group = "Golo" | ||
| 57 | +languages = ["golo"] | ||
| 58 | +body = ''' | ||
| 59 | +let f = |x| -> x * 2''' | ||
| 60 | + | ||
| 61 | +[[snippet]] | ||
| 62 | +name = "struct" | ||
| 63 | +group = "Golo" | ||
| 64 | +languages = ["golo"] | ||
| 65 | +body = ''' | ||
| 66 | +struct Point = { x, y }''' | ||
| 67 | + | ||
| 68 | +[[snippet]] | ||
| 69 | +name = "union" | ||
| 70 | +group = "Golo" | ||
| 71 | +languages = ["golo"] | ||
| 72 | +body = ''' | ||
| 73 | +union Shape = { | ||
| 74 | + Circle = { radius } | ||
| 75 | + Rect = { width, height } | ||
| 76 | +}''' | ||
| 77 | + | ||
| 78 | +[[snippet]] | ||
| 79 | +name = "augment" | ||
| 80 | +group = "Golo" | ||
| 81 | +languages = ["golo"] | ||
| 82 | +body = ''' | ||
| 83 | +augment Point { | ||
| 84 | + function describe = |this| -> "(" + this: x() + ", " + this: y() + ")" | ||
| 85 | +}''' | ||
| 86 | + | ||
| 87 | +[[snippet]] | ||
| 88 | +name = "match" | ||
| 89 | +group = "Golo" | ||
| 90 | +languages = ["golo"] | ||
| 91 | +body = ''' | ||
| 92 | +let label = match { | ||
| 93 | + when n < 0 then "negative" | ||
| 94 | + when n == 0 then "zero" | ||
| 95 | + otherwise "positive" | ||
| 96 | +}''' | ||
| 97 | + | ||
| 98 | +[[snippet]] | ||
| 99 | +name = "foreach" | ||
| 100 | +group = "Golo" | ||
| 101 | +languages = ["golo"] | ||
| 102 | +body = ''' | ||
| 103 | +foreach item in list[1, 2, 3] { | ||
| 104 | + println(item) | ||
| 105 | +}''' | ||
| 106 | + | ||
| 107 | +[[snippet]] | ||
| 108 | +name = "for" | ||
| 109 | +group = "Golo" | ||
| 110 | +languages = ["golo"] | ||
| 111 | +body = ''' | ||
| 112 | +for (var i = 0, i < 10, i = i + 1) { | ||
| 113 | + println(i) | ||
| 114 | +}''' | ||
| 115 | + | ||
| 116 | +[[snippet]] | ||
| 117 | +name = "try" | ||
| 118 | +group = "Golo" | ||
| 119 | +languages = ["golo"] | ||
| 120 | +body = ''' | ||
| 121 | +try { | ||
| 122 | + throw "boom" | ||
| 123 | +} catch (e) { | ||
| 124 | + println("caught: \"" + e + "\"") | ||
| 125 | +} finally { | ||
| 126 | + println("done") | ||
| 127 | +}''' | ||
| 128 | + | ||
| 129 | +[[snippet]] | ||
| 130 | +name = "comprehension" | ||
| 131 | +group = "Golo" | ||
| 132 | +languages = ["golo"] | ||
| 133 | +body = ''' | ||
| 134 | +let squares = list[x * x foreach x in range(1, 6) when x > 2]''' | ||
| 135 | + | ||
| 136 | +[[snippet]] | ||
| 137 | +group = "General" | ||
| 138 | +name = "Hello" | ||
| 139 | +body = "Hello!!!" | ||
| 140 | + | ||
| 141 | +[[snippet]] | ||
| 142 | +group = "Markdown" | ||
| 143 | +name = "Image" | ||
| 144 | +languages = ["markdown"] | ||
| 145 | +body = "" | ||
added
internal/gololang/templates.go +72 -0 | new file mode 100644 | ||
| @@ -0,0 +1,72 @@ | ||
| 1 | +package gololang | |
| 2 | + | |
| 3 | +import _ "embed" | |
| 4 | + | |
| 5 | +// The starter files Turbo Golo writes into a project's .turbo-golo directory. | |
| 6 | +// | |
| 7 | +// They live in four files beside this one and are embedded into the binary at | |
| 8 | +// compile time. Written out as text rather than encoded from structs because | |
| 9 | +// they are meant to be read and edited by a person: the comments in them say | |
| 10 | +// what each key is for, which is the whole reason the editor offers to create | |
| 11 | +// them at all rather than only to read them. | |
| 12 | +// | |
| 13 | +// Their contents are the one part of these four files that is about Golo | |
| 14 | +// rather than about editing, which is why they live here and not in turbo-core. | |
| 15 | +// | |
| 16 | +// **The .tmpl suffix is not decoration.** Each file is formatted with | |
| 17 | +// fmt.Sprintf before it is written, and settings.toml.tmpl holds `theme = %q` | |
| 18 | +// — which is not valid TOML. Naming it settings.toml would be a claim it | |
| 19 | +// cannot meet: a TOML linter would reject it, and Turbo Golo itself would | |
| 20 | +// colour it as TOML and draw it as broken. The blanks each one takes are | |
| 21 | +// documented on profile.Templates, and templates_test.go holds them to it. | |
| 22 | + | |
| 23 | +// settingsTemplate is the settings file a project gets when it asks for one. | |
| 24 | +// | |
| 25 | +// autosave is on: a project that has gone to the trouble of creating a | |
| 26 | +// settings file has said what it wants, and the file is the visible, editable | |
| 27 | +// place to say otherwise. settings.Default() — what applies with no file at | |
| 28 | +// all — stays off. | |
| 29 | +// | |
| 30 | +//go:embed settings.toml.tmpl | |
| 31 | +var settingsTemplate string | |
| 32 | + | |
| 33 | +// snippetsTemplate is the snippets file a project gets when it asks for one. | |
| 34 | +// | |
| 35 | +// It lists every language name the editor knows in its `languages` comment, | |
| 36 | +// because that comment is where a user finds out what they may write there. A | |
| 37 | +// test iterates syntax.Registered() rather than a hardcoded list, so the | |
| 38 | +// comment cannot fall behind the registry. | |
| 39 | +// | |
| 40 | +// Every Golo body in it is a TOML *literal* multi-line string — the form | |
| 41 | +// written with three apostrophes rather than three double quotes. (Spelling | |
| 42 | +// that out in words is deliberate: gofmt rewrites a bare run of apostrophes in | |
| 43 | +// a doc comment into typographic quotes.) Golo strings carry \n and \" the way | |
| 44 | +// Go's do, and TOML's basic strings would interpret those escapes before the | |
| 45 | +// editor ever saw them — so a snippet with a newline in a string would be | |
| 46 | +// inserted with a real line break in it. In a literal string a backslash is | |
| 47 | +// just a backslash, which is what a Golo snippet needs. | |
| 48 | +// | |
| 49 | +//go:embed snippets.toml.tmpl | |
| 50 | +var snippetsTemplate string | |
| 51 | + | |
| 52 | +// toolsTemplate is the tools file a project gets when it asks for one. | |
| 53 | +// | |
| 54 | +// Nine commands, and the two features that are invisible otherwise: a | |
| 55 | +// {{placeholder}} that asks for a value before the command runs, and the | |
| 56 | +// `menu` key that puts a tool in a menu of its own. | |
| 57 | +// | |
| 58 | +// Run comes first, because Golo is a scripting language and running the file | |
| 59 | +// is the thing a Golo programmer does most. It gets a terminal rather than a | |
| 60 | +// popup, because a script that reads the keyboard has to be answerable. | |
| 61 | +// | |
| 62 | +//go:embed tools.toml.tmpl | |
| 63 | +var toolsTemplate string | |
| 64 | + | |
| 65 | +// agentsTemplate is the agents file a project gets when it asks for one. | |
| 66 | +// | |
| 67 | +// It takes two blanks, in this order: the editor's own project directory — | |
| 68 | +// which the example agent's arguments point into — and the path to the user's | |
| 69 | +// own agents file, which a comment names. | |
| 70 | +// | |
| 71 | +//go:embed acp.toml.tmpl | |
| 72 | +var agentsTemplate string | |
| new file mode 100644 | |||
| @@ -0,0 +1,72 @@ | |||
| 1 | +package gololang | ||
| 2 | + | ||
| 3 | +import _ "embed" | ||
| 4 | + | ||
| 5 | +// The starter files Turbo Golo writes into a project's .turbo-golo directory. | ||
| 6 | +// | ||
| 7 | +// They live in four files beside this one and are embedded into the binary at | ||
| 8 | +// compile time. Written out as text rather than encoded from structs because | ||
| 9 | +// they are meant to be read and edited by a person: the comments in them say | ||
| 10 | +// what each key is for, which is the whole reason the editor offers to create | ||
| 11 | +// them at all rather than only to read them. | ||
| 12 | +// | ||
| 13 | +// Their contents are the one part of these four files that is about Golo | ||
| 14 | +// rather than about editing, which is why they live here and not in turbo-core. | ||
| 15 | +// | ||
| 16 | +// **The .tmpl suffix is not decoration.** Each file is formatted with | ||
| 17 | +// fmt.Sprintf before it is written, and settings.toml.tmpl holds `theme = %q` | ||
| 18 | +// — which is not valid TOML. Naming it settings.toml would be a claim it | ||
| 19 | +// cannot meet: a TOML linter would reject it, and Turbo Golo itself would | ||
| 20 | +// colour it as TOML and draw it as broken. The blanks each one takes are | ||
| 21 | +// documented on profile.Templates, and templates_test.go holds them to it. | ||
| 22 | + | ||
| 23 | +// settingsTemplate is the settings file a project gets when it asks for one. | ||
| 24 | +// | ||
| 25 | +// autosave is on: a project that has gone to the trouble of creating a | ||
| 26 | +// settings file has said what it wants, and the file is the visible, editable | ||
| 27 | +// place to say otherwise. settings.Default() — what applies with no file at | ||
| 28 | +// all — stays off. | ||
| 29 | +// | ||
| 30 | +//go:embed settings.toml.tmpl | ||
| 31 | +var settingsTemplate string | ||
| 32 | + | ||
| 33 | +// snippetsTemplate is the snippets file a project gets when it asks for one. | ||
| 34 | +// | ||
| 35 | +// It lists every language name the editor knows in its `languages` comment, | ||
| 36 | +// because that comment is where a user finds out what they may write there. A | ||
| 37 | +// test iterates syntax.Registered() rather than a hardcoded list, so the | ||
| 38 | +// comment cannot fall behind the registry. | ||
| 39 | +// | ||
| 40 | +// Every Golo body in it is a TOML *literal* multi-line string — the form | ||
| 41 | +// written with three apostrophes rather than three double quotes. (Spelling | ||
| 42 | +// that out in words is deliberate: gofmt rewrites a bare run of apostrophes in | ||
| 43 | +// a doc comment into typographic quotes.) Golo strings carry \n and \" the way | ||
| 44 | +// Go's do, and TOML's basic strings would interpret those escapes before the | ||
| 45 | +// editor ever saw them — so a snippet with a newline in a string would be | ||
| 46 | +// inserted with a real line break in it. In a literal string a backslash is | ||
| 47 | +// just a backslash, which is what a Golo snippet needs. | ||
| 48 | +// | ||
| 49 | +//go:embed snippets.toml.tmpl | ||
| 50 | +var snippetsTemplate string | ||
| 51 | + | ||
| 52 | +// toolsTemplate is the tools file a project gets when it asks for one. | ||
| 53 | +// | ||
| 54 | +// Nine commands, and the two features that are invisible otherwise: a | ||
| 55 | +// {{placeholder}} that asks for a value before the command runs, and the | ||
| 56 | +// `menu` key that puts a tool in a menu of its own. | ||
| 57 | +// | ||
| 58 | +// Run comes first, because Golo is a scripting language and running the file | ||
| 59 | +// is the thing a Golo programmer does most. It gets a terminal rather than a | ||
| 60 | +// popup, because a script that reads the keyboard has to be answerable. | ||
| 61 | +// | ||
| 62 | +//go:embed tools.toml.tmpl | ||
| 63 | +var toolsTemplate string | ||
| 64 | + | ||
| 65 | +// agentsTemplate is the agents file a project gets when it asks for one. | ||
| 66 | +// | ||
| 67 | +// It takes two blanks, in this order: the editor's own project directory — | ||
| 68 | +// which the example agent's arguments point into — and the path to the user's | ||
| 69 | +// own agents file, which a comment names. | ||
| 70 | +// | ||
| 71 | +//go:embed acp.toml.tmpl | ||
| 72 | +var agentsTemplate string | ||
added
internal/gololang/templates_test.go +453 -0 | new file mode 100644 | ||
| @@ -0,0 +1,453 @@ | ||
| 1 | +package gololang | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "fmt" | |
| 5 | + "os" | |
| 6 | + "regexp" | |
| 7 | + "strings" | |
| 8 | + "testing" | |
| 9 | + | |
| 10 | + "rickub.com/turbo-editors/turbo-core/settings" | |
| 11 | + "rickub.com/turbo-editors/turbo-core/snippets" | |
| 12 | + "rickub.com/turbo-editors/turbo-core/syntax" | |
| 13 | + "rickub.com/turbo-editors/turbo-core/tools" | |
| 14 | +) | |
| 15 | + | |
| 16 | +// The starter files Turbo Golo writes are the one part of a project's | |
| 17 | +// .turbo-golo directory that is about Golo, so this is where what is *in* | |
| 18 | +// them is checked. That the file written is the profile's template at all is | |
| 19 | +// turbo-core's test. | |
| 20 | + | |
| 21 | +// noUserSnippets points the user's own snippets at an empty directory, so a | |
| 22 | +// test never reads whoever is running it. | |
| 23 | +func noUserSnippets(t *testing.T) { | |
| 24 | + t.Helper() | |
| 25 | + t.Setenv(Profile().SnippetDirEnvVar(), t.TempDir()) | |
| 26 | +} | |
| 27 | + | |
| 28 | +// createTools writes a project's tools file and returns the project directory. | |
| 29 | +func createTools(t *testing.T) string { | |
| 30 | + t.Helper() | |
| 31 | + | |
| 32 | + dir := t.TempDir() | |
| 33 | + if _, err := tools.Create(Profile(), dir); err != nil { | |
| 34 | + t.Fatalf("tools.Create() error = %v", err) | |
| 35 | + } | |
| 36 | + return dir | |
| 37 | +} | |
| 38 | + | |
| 39 | +// createSnippets writes a project's snippets file and returns the directory. | |
| 40 | +func createSnippets(t *testing.T) string { | |
| 41 | + t.Helper() | |
| 42 | + noUserSnippets(t) | |
| 43 | + | |
| 44 | + dir := t.TempDir() | |
| 45 | + if _, err := snippets.Create(Profile(), dir); err != nil { | |
| 46 | + t.Fatalf("snippets.Create() error = %v", err) | |
| 47 | + } | |
| 48 | + return dir | |
| 49 | +} | |
| 50 | + | |
| 51 | +// createSettings writes a project's settings file and returns the directory. | |
| 52 | +func createSettings(t *testing.T) string { | |
| 53 | + t.Helper() | |
| 54 | + | |
| 55 | + dir := t.TempDir() | |
| 56 | + if _, err := settings.Create(Profile(), dir, "turbo-classic"); err != nil { | |
| 57 | + t.Fatalf("settings.Create() error = %v", err) | |
| 58 | + } | |
| 59 | + return dir | |
| 60 | +} | |
| 61 | + | |
| 62 | +// readFile returns a file's contents. | |
| 63 | +func readFile(t *testing.T, path string) string { | |
| 64 | + t.Helper() | |
| 65 | + | |
| 66 | + data, err := os.ReadFile(path) | |
| 67 | + if err != nil { | |
| 68 | + t.Fatalf("reading %s: %v", path, err) | |
| 69 | + } | |
| 70 | + return string(data) | |
| 71 | +} | |
| 72 | + | |
| 73 | +// --- the formatting contract ------------------------------------------------ | |
| 74 | + | |
| 75 | +// profile.Templates documents how many verbs each template takes, and nothing | |
| 76 | +// enforces it. A template with the wrong number produces %!q(MISSING) or | |
| 77 | +// %!(EXTRA …) in a file that is written into somebody's project, opened, and | |
| 78 | +// wrong — Go writes the marker into the output rather than failing. | |
| 79 | + | |
| 80 | +func TestEachTemplateTakesTheVerbsItsContractSays(t *testing.T) { | |
| 81 | + cases := []struct { | |
| 82 | + name string | |
| 83 | + template string | |
| 84 | + verb string | |
| 85 | + want int | |
| 86 | + }{ | |
| 87 | + {"Settings", settingsTemplate, "%q", 2}, | |
| 88 | + {"Snippets", snippetsTemplate, "%s", 2}, | |
| 89 | + {"Tools", toolsTemplate, "%", 0}, | |
| 90 | + } | |
| 91 | + | |
| 92 | + for _, c := range cases { | |
| 93 | + if got := strings.Count(c.template, c.verb); got != c.want { | |
| 94 | + t.Errorf("%s template has %d %q verbs, want %d", c.name, got, c.verb, c.want) | |
| 95 | + } | |
| 96 | + } | |
| 97 | +} | |
| 98 | + | |
| 99 | +func TestFillingATemplateLeavesNoMissingMarker(t *testing.T) { | |
| 100 | + filled := map[string]string{ | |
| 101 | + "settings": fmt.Sprintf(settingsTemplate, "turbo-classic", "500ms"), | |
| 102 | + "snippets": fmt.Sprintf(snippetsTemplate, "Snippets", "/home/someone/.config/turbo-golo/snippets.toml"), | |
| 103 | + "tools": toolsTemplate, | |
| 104 | + } | |
| 105 | + | |
| 106 | + for name, text := range filled { | |
| 107 | + if at := strings.Index(text, "%!"); at >= 0 { | |
| 108 | + t.Errorf("the %s template filled in with %q — the wrong number of verbs", name, text[at:min(at+24, len(text))]) | |
| 109 | + } | |
| 110 | + } | |
| 111 | +} | |
| 112 | + | |
| 113 | +// --- what the files say ----------------------------------------------------- | |
| 114 | + | |
| 115 | +func TestNoTemplateNamesTheEditorThisOneWasAdaptedFrom(t *testing.T) { | |
| 116 | + // A leftover turbo-moonbit in a file written into somebody's Golo | |
| 117 | + // project is invisible to every other test here. | |
| 118 | + // | |
| 119 | + // Whole words, because turbo-go is a prefix of turbo-golo: a plain | |
| 120 | + // substring check would fail on this editor's own name. | |
| 121 | + strangers := regexp.MustCompile(`(?i)\b(turbo-moonbit|turbo-python|turbo-rust|turbo-go|moonbitlang|pythonlang|rustlang|golang|moonbit|moon|mbt|pyproject|cargo|pytest|clippy|gopls|pylsp)\b`) | |
| 122 | + | |
| 123 | + for name, template := range map[string]string{ | |
| 124 | + "settings": settingsTemplate, | |
| 125 | + "snippets": snippetsTemplate, | |
| 126 | + "tools": toolsTemplate, | |
| 127 | + } { | |
| 128 | + if stranger := strangers.FindString(template); stranger != "" { | |
| 129 | + t.Errorf("the %s template still says %q", name, stranger) | |
| 130 | + } | |
| 131 | + } | |
| 132 | +} | |
| 133 | + | |
| 134 | +func TestTheSettingsFileTurnsAutosaveOn(t *testing.T) { | |
| 135 | + // A project that has gone to the trouble of creating a settings file has | |
| 136 | + // said what it wants. settings.Default() — what applies with no file at | |
| 137 | + // all — stays off, and that is checked below. | |
| 138 | + dir := createSettings(t) | |
| 139 | + | |
| 140 | + loaded, err := settings.Load(Profile(), dir) | |
| 141 | + if err != nil { | |
| 142 | + t.Fatalf("settings.Load() error = %v", err) | |
| 143 | + } | |
| 144 | + if !loaded.Autosave { | |
| 145 | + t.Error("the starter settings file leaves autosave off, want it on") | |
| 146 | + } | |
| 147 | + if settings.Default().Autosave { | |
| 148 | + t.Error("settings.Default() has autosave on; the two statements have drifted together") | |
| 149 | + } | |
| 150 | +} | |
| 151 | + | |
| 152 | +func TestTheSettingsFileNamesTheThemeItWasCreatedWith(t *testing.T) { | |
| 153 | + dir := createSettings(t) | |
| 154 | + | |
| 155 | + loaded, err := settings.Load(Profile(), dir) | |
| 156 | + if err != nil { | |
| 157 | + t.Fatalf("settings.Load() error = %v", err) | |
| 158 | + } | |
| 159 | + if loaded.Theme != "turbo-classic" { | |
| 160 | + t.Errorf("theme = %q, want %q", loaded.Theme, "turbo-classic") | |
| 161 | + } | |
| 162 | +} | |
| 163 | + | |
| 164 | +func TestTheSnippetsCommentNamesEveryLanguageTheEditorKnows(t *testing.T) { | |
| 165 | + // The comment is where a user finds out what they may write in a | |
| 166 | + // `languages` key. It fell behind the registry once already in this family, | |
| 167 | + // when turbo-core learnt YAML, XML and Dockerfiles — so the list is read | |
| 168 | + // from the registry rather than written down here. | |
| 169 | + Register() | |
| 170 | + | |
| 171 | + list := languageListOf(t, snippetsTemplate) | |
| 172 | + for _, language := range syntax.Registered() { | |
| 173 | + if !strings.Contains(list, language.String()) { | |
| 174 | + t.Errorf("the snippets template's languages comment does not name %q; it reads %q", language, list) | |
| 175 | + } | |
| 176 | + } | |
| 177 | +} | |
| 178 | + | |
| 179 | +// languageListOf returns the one sentence of the snippets template that lists | |
| 180 | +// the language names, with its comment marks stripped. | |
| 181 | +// | |
| 182 | +// Only that sentence will do. Every snippet body below it carries a languages | |
| 183 | +// key naming Golo, and the file's own first line names turbo-golo — so a | |
| 184 | +// check against the whole template, or even against all of its comments, would | |
| 185 | +// pass with the list itself saying nothing at all. | |
| 186 | +func languageListOf(t *testing.T, template string) string { | |
| 187 | + t.Helper() | |
| 188 | + | |
| 189 | + const marker = "editor uses:" | |
| 190 | + at := strings.Index(template, marker) | |
| 191 | + if at < 0 { | |
| 192 | + t.Fatalf("the snippets template no longer introduces its language list with %q", marker) | |
| 193 | + } | |
| 194 | + | |
| 195 | + rest := template[at+len(marker):] | |
| 196 | + end := strings.Index(rest, ".") | |
| 197 | + if end < 0 { | |
| 198 | + t.Fatal("the snippets template's language list does not end in a full stop") | |
| 199 | + } | |
| 200 | + return strings.ReplaceAll(rest[:end], "#", "") | |
| 201 | +} | |
| 202 | + | |
| 203 | +func TestEverySnippetLoadsAndIsForGolo(t *testing.T) { | |
| 204 | + Register() | |
| 205 | + dir := createSnippets(t) | |
| 206 | + | |
| 207 | + list, err := snippets.Load(Profile(), dir) | |
| 208 | + if err != nil { | |
| 209 | + t.Fatalf("snippets.Load() error = %v", err) | |
| 210 | + } | |
| 211 | + if list.Len() == 0 { | |
| 212 | + t.Fatal("the starter snippets file holds none") | |
| 213 | + } | |
| 214 | + | |
| 215 | + groups := list.Groups(Language.String()) | |
| 216 | + var found bool | |
| 217 | + for _, group := range groups { | |
| 218 | + if group.Name == "Golo" { | |
| 219 | + found = true | |
| 220 | + } | |
| 221 | + } | |
| 222 | + if !found { | |
| 223 | + t.Errorf("no Golo group among %v", groups) | |
| 224 | + } | |
| 225 | +} | |
| 226 | + | |
| 227 | +func TestSnippetBodiesAreIndentedTheWayGoloExamplesAre(t *testing.T) { | |
| 228 | + // Every example in the GoloScript documentation and its own templates | |
| 229 | + // indents with two spaces. Golo has no formatter, so the convention is the | |
| 230 | + // only authority, and a snippet that disagrees with it stands out in every | |
| 231 | + // file it is inserted into. | |
| 232 | + Register() | |
| 233 | + dir := createSnippets(t) | |
| 234 | + | |
| 235 | + list, err := snippets.Load(Profile(), dir) | |
| 236 | + if err != nil { | |
| 237 | + t.Fatalf("snippets.Load() error = %v", err) | |
| 238 | + } | |
| 239 | + | |
| 240 | + for _, group := range list.Groups(Language.String()) { | |
| 241 | + for _, snippet := range group.Snippets { | |
| 242 | + for _, line := range strings.Split(snippet.Body, "\n") { | |
| 243 | + if strings.Contains(line, "\t") { | |
| 244 | + t.Errorf("snippet %q has a tab in %q", snippet.Name, line) | |
| 245 | + } | |
| 246 | + indent := len(line) - len(strings.TrimLeft(line, " ")) | |
| 247 | + if indent%2 != 0 { | |
| 248 | + t.Errorf("snippet %q indents %q by %d spaces, want a multiple of two", snippet.Name, line, indent) | |
| 249 | + } | |
| 250 | + } | |
| 251 | + } | |
| 252 | + } | |
| 253 | +} | |
| 254 | + | |
| 255 | +func TestTheSnippetsFileIsTOMLWithLiteralBodies(t *testing.T) { | |
| 256 | + // A Golo string carries \n and \" the way a Go string does, and TOML | |
| 257 | + // interprets those escapes in a basic string before the editor ever sees | |
| 258 | + // them — so a snippet with an escaped quote would be inserted with the | |
| 259 | + // escape already resolved and the Golo broken. That the file parses is what | |
| 260 | + // createSnippets proves; that it really does hold a backslash is what makes | |
| 261 | + // the proof mean something. | |
| 262 | + Register() | |
| 263 | + dir := createSnippets(t) | |
| 264 | + | |
| 265 | + written := readFile(t, snippets.ProjectPath(Profile(), dir)) | |
| 266 | + if !strings.Contains(written, `\"`) { | |
| 267 | + t.Fatal("no snippet in the starter file escapes a quote, so nothing here tests the literal-string decision") | |
| 268 | + } | |
| 269 | + for _, line := range strings.Split(written, "\n") { | |
| 270 | + if strings.HasPrefix(line, `body = """`) { | |
| 271 | + t.Errorf("a body is opened with a TOML basic multi-line string: %q", line) | |
| 272 | + } | |
| 273 | + } | |
| 274 | +} | |
| 275 | + | |
| 276 | +func TestEveryToolLoadsAndRunsGoloScript(t *testing.T) { | |
| 277 | + dir := createTools(t) | |
| 278 | + | |
| 279 | + list, err := tools.Load(Profile(), dir) | |
| 280 | + if err != nil { | |
| 281 | + t.Fatalf("tools.Load() error = %v", err) | |
| 282 | + } | |
| 283 | + if list.Len() == 0 { | |
| 284 | + t.Fatal("the starter tools file holds none") | |
| 285 | + } | |
| 286 | + | |
| 287 | + for _, tool := range list.In("Golo") { | |
| 288 | + if !runsGoloScript(tool.Command) { | |
| 289 | + t.Errorf("tool %q in the Golo menu runs %q, which is none of golo, gogolo or wagolo", tool.Name, tool.Command) | |
| 290 | + } | |
| 291 | + } | |
| 292 | +} | |
| 293 | + | |
| 294 | +// runsGoloScript reports whether a command starts one of GoloScript's three | |
| 295 | +// binaries: the interpreter, or either compiler. | |
| 296 | +func runsGoloScript(command string) bool { | |
| 297 | + for _, binary := range []string{"golo", "gogolo", "wagolo"} { | |
| 298 | + if command == binary || strings.HasPrefix(command, binary+" ") { | |
| 299 | + return true | |
| 300 | + } | |
| 301 | + } | |
| 302 | + return false | |
| 303 | +} | |
| 304 | + | |
| 305 | +func TestTheToolsFileShowsBothInvisibleFeatures(t *testing.T) { | |
| 306 | + // A {{placeholder}} and the `menu` key are invisible unless the starter | |
| 307 | + // file demonstrates them, and the starter file is where anyone learns they | |
| 308 | + // exist at all. | |
| 309 | + dir := createTools(t) | |
| 310 | + | |
| 311 | + list, err := tools.Load(Profile(), dir) | |
| 312 | + if err != nil { | |
| 313 | + t.Fatalf("tools.Load() error = %v", err) | |
| 314 | + } | |
| 315 | + | |
| 316 | + var asks, elsewhere int | |
| 317 | + for _, tool := range list.Tools() { | |
| 318 | + if len(tool.Placeholders()) > 0 { | |
| 319 | + asks++ | |
| 320 | + } | |
| 321 | + if tool.Menu != list.DefaultMenu() { | |
| 322 | + elsewhere++ | |
| 323 | + } | |
| 324 | + } | |
| 325 | + if asks == 0 { | |
| 326 | + t.Error("no tool asks for a value, so nothing shows the {{placeholder}} form") | |
| 327 | + } | |
| 328 | + if elsewhere == 0 { | |
| 329 | + t.Error("no tool names a menu of its own, so nothing shows the menu key") | |
| 330 | + } | |
| 331 | +} | |
| 332 | + | |
| 333 | +func TestTheDefaultMenuIsTheGoloOne(t *testing.T) { | |
| 334 | + dir := createTools(t) | |
| 335 | + | |
| 336 | + list, err := tools.Load(Profile(), dir) | |
| 337 | + if err != nil { | |
| 338 | + t.Fatalf("tools.Load() error = %v", err) | |
| 339 | + } | |
| 340 | + if got := list.DefaultMenu(); got != "Golo" { | |
| 341 | + t.Errorf("DefaultMenu() = %q, want %q", got, "Golo") | |
| 342 | + } | |
| 343 | +} | |
| 344 | + | |
| 345 | +func TestNoTwoToolsInOneMenuClaimTheSameHotKey(t *testing.T) { | |
| 346 | + dir := createTools(t) | |
| 347 | + | |
| 348 | + list, err := tools.Load(Profile(), dir) | |
| 349 | + if err != nil { | |
| 350 | + t.Fatalf("tools.Load() error = %v", err) | |
| 351 | + } | |
| 352 | + | |
| 353 | + for _, menu := range list.MenuNames() { | |
| 354 | + taken := map[rune]string{} | |
| 355 | + for _, tool := range list.In(menu) { | |
| 356 | + key, ok := hotKey(tool.Name) | |
| 357 | + if !ok { | |
| 358 | + continue | |
| 359 | + } | |
| 360 | + if other, clash := taken[key]; clash { | |
| 361 | + t.Errorf("in the %s menu, %q and %q both claim %q", menu, other, tool.Name, key) | |
| 362 | + } | |
| 363 | + taken[key] = tool.Name | |
| 364 | + } | |
| 365 | + } | |
| 366 | +} | |
| 367 | + | |
| 368 | +// hotKey returns the upper-case letter a tool's name marks between tildes. | |
| 369 | +func hotKey(name string) (rune, bool) { | |
| 370 | + open := strings.Index(name, "~") | |
| 371 | + if open < 0 || len(name) < open+3 || name[open+2] != '~' { | |
| 372 | + return 0, false | |
| 373 | + } | |
| 374 | + return []rune(strings.ToUpper(name[open+1 : open+2]))[0], true | |
| 375 | +} | |
| 376 | + | |
| 377 | +func TestTheRunToolGetsATerminal(t *testing.T) { | |
| 378 | + // A program that reads the keyboard has to be answerable, and one that runs | |
| 379 | + // long has to be interruptible. A popup is neither. | |
| 380 | + dir := createTools(t) | |
| 381 | + | |
| 382 | + list, err := tools.Load(Profile(), dir) | |
| 383 | + if err != nil { | |
| 384 | + t.Fatalf("tools.Load() error = %v", err) | |
| 385 | + } | |
| 386 | + | |
| 387 | + for _, tool := range list.Tools() { | |
| 388 | + if strings.HasPrefix(tool.Command, "golo {{") && tool.Output != tools.OutputTerminal { | |
| 389 | + t.Errorf("the run tool %q sends its output to %q, want a terminal", tool.Name, tool.Output) | |
| 390 | + } | |
| 391 | + } | |
| 392 | +} | |
| 393 | + | |
| 394 | +func TestEveryPlaceholderAsksForSomething(t *testing.T) { | |
| 395 | + // A half-typed {{ is refused when the file is read, which tools.Load | |
| 396 | + // already proves. This checks the other half: that each label says what it | |
| 397 | + // wants, because the label is the whole of what the box shows. | |
| 398 | + dir := createTools(t) | |
| 399 | + | |
| 400 | + list, err := tools.Load(Profile(), dir) | |
| 401 | + if err != nil { | |
| 402 | + t.Fatalf("tools.Load() error = %v", err) | |
| 403 | + } | |
| 404 | + | |
| 405 | + for _, tool := range list.Tools() { | |
| 406 | + for _, placeholder := range tool.Placeholders() { | |
| 407 | + if strings.TrimSpace(placeholder.Label) == "" { | |
| 408 | + t.Errorf("tool %q has a placeholder with no label", tool.Name) | |
| 409 | + } | |
| 410 | + } | |
| 411 | + } | |
| 412 | +} | |
| 413 | + | |
| 414 | +// The tools reference prints the starter file's table. Turbo Python's shipped | |
| 415 | +// five rows for a file that had six, and claimed `Alt-T` for a menu whose key | |
| 416 | +// is `Alt-P` — both inherited from Turbo Rust by a mechanical substitution that | |
| 417 | +// only looked at identifiers. Nothing in either repository could see it. | |
| 418 | +// | |
| 419 | +// So the table is read out of the page and held to the file the editor | |
| 420 | +// actually writes, in both languages. | |
| 421 | +func TestTheToolsReferenceMatchesTheStarterFile(t *testing.T) { | |
| 422 | + dir := createTools(t) | |
| 423 | + | |
| 424 | + list, err := tools.Load(Profile(), dir) | |
| 425 | + if err != nil { | |
| 426 | + t.Fatalf("tools.Load() error = %v", err) | |
| 427 | + } | |
| 428 | + | |
| 429 | + for _, page := range []string{"../../docs/en/reference/golo-tools.md", "../../docs/fr/reference/golo-tools.md"} { | |
| 430 | + raw, err := os.ReadFile(page) | |
| 431 | + if err != nil { | |
| 432 | + t.Fatalf("reading %s: %v", page, err) | |
| 433 | + } | |
| 434 | + text := string(raw) | |
| 435 | + | |
| 436 | + for _, tool := range list.Tools() { | |
| 437 | + if !strings.Contains(text, "| `"+tool.Name+"` |") { | |
| 438 | + t.Errorf("%s has no row for the tool %q", page, tool.Name) | |
| 439 | + } | |
| 440 | + if !strings.Contains(text, "`"+tool.Command+"`") { | |
| 441 | + t.Errorf("%s does not print the command %q", page, tool.Command) | |
| 442 | + } | |
| 443 | + } | |
| 444 | + if !strings.Contains(text, "`Alt-G`") { | |
| 445 | + t.Errorf("%s never names Alt-G, the key the Golo menu really answers to", page) | |
| 446 | + } | |
| 447 | + for _, stale := range []string{"`Alt-M`", "`Alt-T`, then", "`Alt-T`, puis", "`Alt-P`"} { | |
| 448 | + if strings.Contains(text, stale) { | |
| 449 | + t.Errorf("%s still opens the toolchain menu with %s, which belongs to another editor", page, stale) | |
| 450 | + } | |
| 451 | + } | |
| 452 | + } | |
| 453 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,453 @@ | |||
| 1 | +package gololang | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "fmt" | ||
| 5 | + "os" | ||
| 6 | + "regexp" | ||
| 7 | + "strings" | ||
| 8 | + "testing" | ||
| 9 | + | ||
| 10 | + "rickub.com/turbo-editors/turbo-core/settings" | ||
| 11 | + "rickub.com/turbo-editors/turbo-core/snippets" | ||
| 12 | + "rickub.com/turbo-editors/turbo-core/syntax" | ||
| 13 | + "rickub.com/turbo-editors/turbo-core/tools" | ||
| 14 | +) | ||
| 15 | + | ||
| 16 | +// The starter files Turbo Golo writes are the one part of a project's | ||
| 17 | +// .turbo-golo directory that is about Golo, so this is where what is *in* | ||
| 18 | +// them is checked. That the file written is the profile's template at all is | ||
| 19 | +// turbo-core's test. | ||
| 20 | + | ||
| 21 | +// noUserSnippets points the user's own snippets at an empty directory, so a | ||
| 22 | +// test never reads whoever is running it. | ||
| 23 | +func noUserSnippets(t *testing.T) { | ||
| 24 | + t.Helper() | ||
| 25 | + t.Setenv(Profile().SnippetDirEnvVar(), t.TempDir()) | ||
| 26 | +} | ||
| 27 | + | ||
| 28 | +// createTools writes a project's tools file and returns the project directory. | ||
| 29 | +func createTools(t *testing.T) string { | ||
| 30 | + t.Helper() | ||
| 31 | + | ||
| 32 | + dir := t.TempDir() | ||
| 33 | + if _, err := tools.Create(Profile(), dir); err != nil { | ||
| 34 | + t.Fatalf("tools.Create() error = %v", err) | ||
| 35 | + } | ||
| 36 | + return dir | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +// createSnippets writes a project's snippets file and returns the directory. | ||
| 40 | +func createSnippets(t *testing.T) string { | ||
| 41 | + t.Helper() | ||
| 42 | + noUserSnippets(t) | ||
| 43 | + | ||
| 44 | + dir := t.TempDir() | ||
| 45 | + if _, err := snippets.Create(Profile(), dir); err != nil { | ||
| 46 | + t.Fatalf("snippets.Create() error = %v", err) | ||
| 47 | + } | ||
| 48 | + return dir | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +// createSettings writes a project's settings file and returns the directory. | ||
| 52 | +func createSettings(t *testing.T) string { | ||
| 53 | + t.Helper() | ||
| 54 | + | ||
| 55 | + dir := t.TempDir() | ||
| 56 | + if _, err := settings.Create(Profile(), dir, "turbo-classic"); err != nil { | ||
| 57 | + t.Fatalf("settings.Create() error = %v", err) | ||
| 58 | + } | ||
| 59 | + return dir | ||
| 60 | +} | ||
| 61 | + | ||
| 62 | +// readFile returns a file's contents. | ||
| 63 | +func readFile(t *testing.T, path string) string { | ||
| 64 | + t.Helper() | ||
| 65 | + | ||
| 66 | + data, err := os.ReadFile(path) | ||
| 67 | + if err != nil { | ||
| 68 | + t.Fatalf("reading %s: %v", path, err) | ||
| 69 | + } | ||
| 70 | + return string(data) | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +// --- the formatting contract ------------------------------------------------ | ||
| 74 | + | ||
| 75 | +// profile.Templates documents how many verbs each template takes, and nothing | ||
| 76 | +// enforces it. A template with the wrong number produces %!q(MISSING) or | ||
| 77 | +// %!(EXTRA …) in a file that is written into somebody's project, opened, and | ||
| 78 | +// wrong — Go writes the marker into the output rather than failing. | ||
| 79 | + | ||
| 80 | +func TestEachTemplateTakesTheVerbsItsContractSays(t *testing.T) { | ||
| 81 | + cases := []struct { | ||
| 82 | + name string | ||
| 83 | + template string | ||
| 84 | + verb string | ||
| 85 | + want int | ||
| 86 | + }{ | ||
| 87 | + {"Settings", settingsTemplate, "%q", 2}, | ||
| 88 | + {"Snippets", snippetsTemplate, "%s", 2}, | ||
| 89 | + {"Tools", toolsTemplate, "%", 0}, | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + for _, c := range cases { | ||
| 93 | + if got := strings.Count(c.template, c.verb); got != c.want { | ||
| 94 | + t.Errorf("%s template has %d %q verbs, want %d", c.name, got, c.verb, c.want) | ||
| 95 | + } | ||
| 96 | + } | ||
| 97 | +} | ||
| 98 | + | ||
| 99 | +func TestFillingATemplateLeavesNoMissingMarker(t *testing.T) { | ||
| 100 | + filled := map[string]string{ | ||
| 101 | + "settings": fmt.Sprintf(settingsTemplate, "turbo-classic", "500ms"), | ||
| 102 | + "snippets": fmt.Sprintf(snippetsTemplate, "Snippets", "/home/someone/.config/turbo-golo/snippets.toml"), | ||
| 103 | + "tools": toolsTemplate, | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + for name, text := range filled { | ||
| 107 | + if at := strings.Index(text, "%!"); at >= 0 { | ||
| 108 | + t.Errorf("the %s template filled in with %q — the wrong number of verbs", name, text[at:min(at+24, len(text))]) | ||
| 109 | + } | ||
| 110 | + } | ||
| 111 | +} | ||
| 112 | + | ||
| 113 | +// --- what the files say ----------------------------------------------------- | ||
| 114 | + | ||
| 115 | +func TestNoTemplateNamesTheEditorThisOneWasAdaptedFrom(t *testing.T) { | ||
| 116 | + // A leftover turbo-moonbit in a file written into somebody's Golo | ||
| 117 | + // project is invisible to every other test here. | ||
| 118 | + // | ||
| 119 | + // Whole words, because turbo-go is a prefix of turbo-golo: a plain | ||
| 120 | + // substring check would fail on this editor's own name. | ||
| 121 | + strangers := regexp.MustCompile(`(?i)\b(turbo-moonbit|turbo-python|turbo-rust|turbo-go|moonbitlang|pythonlang|rustlang|golang|moonbit|moon|mbt|pyproject|cargo|pytest|clippy|gopls|pylsp)\b`) | ||
| 122 | + | ||
| 123 | + for name, template := range map[string]string{ | ||
| 124 | + "settings": settingsTemplate, | ||
| 125 | + "snippets": snippetsTemplate, | ||
| 126 | + "tools": toolsTemplate, | ||
| 127 | + } { | ||
| 128 | + if stranger := strangers.FindString(template); stranger != "" { | ||
| 129 | + t.Errorf("the %s template still says %q", name, stranger) | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | +} | ||
| 133 | + | ||
| 134 | +func TestTheSettingsFileTurnsAutosaveOn(t *testing.T) { | ||
| 135 | + // A project that has gone to the trouble of creating a settings file has | ||
| 136 | + // said what it wants. settings.Default() — what applies with no file at | ||
| 137 | + // all — stays off, and that is checked below. | ||
| 138 | + dir := createSettings(t) | ||
| 139 | + | ||
| 140 | + loaded, err := settings.Load(Profile(), dir) | ||
| 141 | + if err != nil { | ||
| 142 | + t.Fatalf("settings.Load() error = %v", err) | ||
| 143 | + } | ||
| 144 | + if !loaded.Autosave { | ||
| 145 | + t.Error("the starter settings file leaves autosave off, want it on") | ||
| 146 | + } | ||
| 147 | + if settings.Default().Autosave { | ||
| 148 | + t.Error("settings.Default() has autosave on; the two statements have drifted together") | ||
| 149 | + } | ||
| 150 | +} | ||
| 151 | + | ||
| 152 | +func TestTheSettingsFileNamesTheThemeItWasCreatedWith(t *testing.T) { | ||
| 153 | + dir := createSettings(t) | ||
| 154 | + | ||
| 155 | + loaded, err := settings.Load(Profile(), dir) | ||
| 156 | + if err != nil { | ||
| 157 | + t.Fatalf("settings.Load() error = %v", err) | ||
| 158 | + } | ||
| 159 | + if loaded.Theme != "turbo-classic" { | ||
| 160 | + t.Errorf("theme = %q, want %q", loaded.Theme, "turbo-classic") | ||
| 161 | + } | ||
| 162 | +} | ||
| 163 | + | ||
| 164 | +func TestTheSnippetsCommentNamesEveryLanguageTheEditorKnows(t *testing.T) { | ||
| 165 | + // The comment is where a user finds out what they may write in a | ||
| 166 | + // `languages` key. It fell behind the registry once already in this family, | ||
| 167 | + // when turbo-core learnt YAML, XML and Dockerfiles — so the list is read | ||
| 168 | + // from the registry rather than written down here. | ||
| 169 | + Register() | ||
| 170 | + | ||
| 171 | + list := languageListOf(t, snippetsTemplate) | ||
| 172 | + for _, language := range syntax.Registered() { | ||
| 173 | + if !strings.Contains(list, language.String()) { | ||
| 174 | + t.Errorf("the snippets template's languages comment does not name %q; it reads %q", language, list) | ||
| 175 | + } | ||
| 176 | + } | ||
| 177 | +} | ||
| 178 | + | ||
| 179 | +// languageListOf returns the one sentence of the snippets template that lists | ||
| 180 | +// the language names, with its comment marks stripped. | ||
| 181 | +// | ||
| 182 | +// Only that sentence will do. Every snippet body below it carries a languages | ||
| 183 | +// key naming Golo, and the file's own first line names turbo-golo — so a | ||
| 184 | +// check against the whole template, or even against all of its comments, would | ||
| 185 | +// pass with the list itself saying nothing at all. | ||
| 186 | +func languageListOf(t *testing.T, template string) string { | ||
| 187 | + t.Helper() | ||
| 188 | + | ||
| 189 | + const marker = "editor uses:" | ||
| 190 | + at := strings.Index(template, marker) | ||
| 191 | + if at < 0 { | ||
| 192 | + t.Fatalf("the snippets template no longer introduces its language list with %q", marker) | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + rest := template[at+len(marker):] | ||
| 196 | + end := strings.Index(rest, ".") | ||
| 197 | + if end < 0 { | ||
| 198 | + t.Fatal("the snippets template's language list does not end in a full stop") | ||
| 199 | + } | ||
| 200 | + return strings.ReplaceAll(rest[:end], "#", "") | ||
| 201 | +} | ||
| 202 | + | ||
| 203 | +func TestEverySnippetLoadsAndIsForGolo(t *testing.T) { | ||
| 204 | + Register() | ||
| 205 | + dir := createSnippets(t) | ||
| 206 | + | ||
| 207 | + list, err := snippets.Load(Profile(), dir) | ||
| 208 | + if err != nil { | ||
| 209 | + t.Fatalf("snippets.Load() error = %v", err) | ||
| 210 | + } | ||
| 211 | + if list.Len() == 0 { | ||
| 212 | + t.Fatal("the starter snippets file holds none") | ||
| 213 | + } | ||
| 214 | + | ||
| 215 | + groups := list.Groups(Language.String()) | ||
| 216 | + var found bool | ||
| 217 | + for _, group := range groups { | ||
| 218 | + if group.Name == "Golo" { | ||
| 219 | + found = true | ||
| 220 | + } | ||
| 221 | + } | ||
| 222 | + if !found { | ||
| 223 | + t.Errorf("no Golo group among %v", groups) | ||
| 224 | + } | ||
| 225 | +} | ||
| 226 | + | ||
| 227 | +func TestSnippetBodiesAreIndentedTheWayGoloExamplesAre(t *testing.T) { | ||
| 228 | + // Every example in the GoloScript documentation and its own templates | ||
| 229 | + // indents with two spaces. Golo has no formatter, so the convention is the | ||
| 230 | + // only authority, and a snippet that disagrees with it stands out in every | ||
| 231 | + // file it is inserted into. | ||
| 232 | + Register() | ||
| 233 | + dir := createSnippets(t) | ||
| 234 | + | ||
| 235 | + list, err := snippets.Load(Profile(), dir) | ||
| 236 | + if err != nil { | ||
| 237 | + t.Fatalf("snippets.Load() error = %v", err) | ||
| 238 | + } | ||
| 239 | + | ||
| 240 | + for _, group := range list.Groups(Language.String()) { | ||
| 241 | + for _, snippet := range group.Snippets { | ||
| 242 | + for _, line := range strings.Split(snippet.Body, "\n") { | ||
| 243 | + if strings.Contains(line, "\t") { | ||
| 244 | + t.Errorf("snippet %q has a tab in %q", snippet.Name, line) | ||
| 245 | + } | ||
| 246 | + indent := len(line) - len(strings.TrimLeft(line, " ")) | ||
| 247 | + if indent%2 != 0 { | ||
| 248 | + t.Errorf("snippet %q indents %q by %d spaces, want a multiple of two", snippet.Name, line, indent) | ||
| 249 | + } | ||
| 250 | + } | ||
| 251 | + } | ||
| 252 | + } | ||
| 253 | +} | ||
| 254 | + | ||
| 255 | +func TestTheSnippetsFileIsTOMLWithLiteralBodies(t *testing.T) { | ||
| 256 | + // A Golo string carries \n and \" the way a Go string does, and TOML | ||
| 257 | + // interprets those escapes in a basic string before the editor ever sees | ||
| 258 | + // them — so a snippet with an escaped quote would be inserted with the | ||
| 259 | + // escape already resolved and the Golo broken. That the file parses is what | ||
| 260 | + // createSnippets proves; that it really does hold a backslash is what makes | ||
| 261 | + // the proof mean something. | ||
| 262 | + Register() | ||
| 263 | + dir := createSnippets(t) | ||
| 264 | + | ||
| 265 | + written := readFile(t, snippets.ProjectPath(Profile(), dir)) | ||
| 266 | + if !strings.Contains(written, `\"`) { | ||
| 267 | + t.Fatal("no snippet in the starter file escapes a quote, so nothing here tests the literal-string decision") | ||
| 268 | + } | ||
| 269 | + for _, line := range strings.Split(written, "\n") { | ||
| 270 | + if strings.HasPrefix(line, `body = """`) { | ||
| 271 | + t.Errorf("a body is opened with a TOML basic multi-line string: %q", line) | ||
| 272 | + } | ||
| 273 | + } | ||
| 274 | +} | ||
| 275 | + | ||
| 276 | +func TestEveryToolLoadsAndRunsGoloScript(t *testing.T) { | ||
| 277 | + dir := createTools(t) | ||
| 278 | + | ||
| 279 | + list, err := tools.Load(Profile(), dir) | ||
| 280 | + if err != nil { | ||
| 281 | + t.Fatalf("tools.Load() error = %v", err) | ||
| 282 | + } | ||
| 283 | + if list.Len() == 0 { | ||
| 284 | + t.Fatal("the starter tools file holds none") | ||
| 285 | + } | ||
| 286 | + | ||
| 287 | + for _, tool := range list.In("Golo") { | ||
| 288 | + if !runsGoloScript(tool.Command) { | ||
| 289 | + t.Errorf("tool %q in the Golo menu runs %q, which is none of golo, gogolo or wagolo", tool.Name, tool.Command) | ||
| 290 | + } | ||
| 291 | + } | ||
| 292 | +} | ||
| 293 | + | ||
| 294 | +// runsGoloScript reports whether a command starts one of GoloScript's three | ||
| 295 | +// binaries: the interpreter, or either compiler. | ||
| 296 | +func runsGoloScript(command string) bool { | ||
| 297 | + for _, binary := range []string{"golo", "gogolo", "wagolo"} { | ||
| 298 | + if command == binary || strings.HasPrefix(command, binary+" ") { | ||
| 299 | + return true | ||
| 300 | + } | ||
| 301 | + } | ||
| 302 | + return false | ||
| 303 | +} | ||
| 304 | + | ||
| 305 | +func TestTheToolsFileShowsBothInvisibleFeatures(t *testing.T) { | ||
| 306 | + // A {{placeholder}} and the `menu` key are invisible unless the starter | ||
| 307 | + // file demonstrates them, and the starter file is where anyone learns they | ||
| 308 | + // exist at all. | ||
| 309 | + dir := createTools(t) | ||
| 310 | + | ||
| 311 | + list, err := tools.Load(Profile(), dir) | ||
| 312 | + if err != nil { | ||
| 313 | + t.Fatalf("tools.Load() error = %v", err) | ||
| 314 | + } | ||
| 315 | + | ||
| 316 | + var asks, elsewhere int | ||
| 317 | + for _, tool := range list.Tools() { | ||
| 318 | + if len(tool.Placeholders()) > 0 { | ||
| 319 | + asks++ | ||
| 320 | + } | ||
| 321 | + if tool.Menu != list.DefaultMenu() { | ||
| 322 | + elsewhere++ | ||
| 323 | + } | ||
| 324 | + } | ||
| 325 | + if asks == 0 { | ||
| 326 | + t.Error("no tool asks for a value, so nothing shows the {{placeholder}} form") | ||
| 327 | + } | ||
| 328 | + if elsewhere == 0 { | ||
| 329 | + t.Error("no tool names a menu of its own, so nothing shows the menu key") | ||
| 330 | + } | ||
| 331 | +} | ||
| 332 | + | ||
| 333 | +func TestTheDefaultMenuIsTheGoloOne(t *testing.T) { | ||
| 334 | + dir := createTools(t) | ||
| 335 | + | ||
| 336 | + list, err := tools.Load(Profile(), dir) | ||
| 337 | + if err != nil { | ||
| 338 | + t.Fatalf("tools.Load() error = %v", err) | ||
| 339 | + } | ||
| 340 | + if got := list.DefaultMenu(); got != "Golo" { | ||
| 341 | + t.Errorf("DefaultMenu() = %q, want %q", got, "Golo") | ||
| 342 | + } | ||
| 343 | +} | ||
| 344 | + | ||
| 345 | +func TestNoTwoToolsInOneMenuClaimTheSameHotKey(t *testing.T) { | ||
| 346 | + dir := createTools(t) | ||
| 347 | + | ||
| 348 | + list, err := tools.Load(Profile(), dir) | ||
| 349 | + if err != nil { | ||
| 350 | + t.Fatalf("tools.Load() error = %v", err) | ||
| 351 | + } | ||
| 352 | + | ||
| 353 | + for _, menu := range list.MenuNames() { | ||
| 354 | + taken := map[rune]string{} | ||
| 355 | + for _, tool := range list.In(menu) { | ||
| 356 | + key, ok := hotKey(tool.Name) | ||
| 357 | + if !ok { | ||
| 358 | + continue | ||
| 359 | + } | ||
| 360 | + if other, clash := taken[key]; clash { | ||
| 361 | + t.Errorf("in the %s menu, %q and %q both claim %q", menu, other, tool.Name, key) | ||
| 362 | + } | ||
| 363 | + taken[key] = tool.Name | ||
| 364 | + } | ||
| 365 | + } | ||
| 366 | +} | ||
| 367 | + | ||
| 368 | +// hotKey returns the upper-case letter a tool's name marks between tildes. | ||
| 369 | +func hotKey(name string) (rune, bool) { | ||
| 370 | + open := strings.Index(name, "~") | ||
| 371 | + if open < 0 || len(name) < open+3 || name[open+2] != '~' { | ||
| 372 | + return 0, false | ||
| 373 | + } | ||
| 374 | + return []rune(strings.ToUpper(name[open+1 : open+2]))[0], true | ||
| 375 | +} | ||
| 376 | + | ||
| 377 | +func TestTheRunToolGetsATerminal(t *testing.T) { | ||
| 378 | + // A program that reads the keyboard has to be answerable, and one that runs | ||
| 379 | + // long has to be interruptible. A popup is neither. | ||
| 380 | + dir := createTools(t) | ||
| 381 | + | ||
| 382 | + list, err := tools.Load(Profile(), dir) | ||
| 383 | + if err != nil { | ||
| 384 | + t.Fatalf("tools.Load() error = %v", err) | ||
| 385 | + } | ||
| 386 | + | ||
| 387 | + for _, tool := range list.Tools() { | ||
| 388 | + if strings.HasPrefix(tool.Command, "golo {{") && tool.Output != tools.OutputTerminal { | ||
| 389 | + t.Errorf("the run tool %q sends its output to %q, want a terminal", tool.Name, tool.Output) | ||
| 390 | + } | ||
| 391 | + } | ||
| 392 | +} | ||
| 393 | + | ||
| 394 | +func TestEveryPlaceholderAsksForSomething(t *testing.T) { | ||
| 395 | + // A half-typed {{ is refused when the file is read, which tools.Load | ||
| 396 | + // already proves. This checks the other half: that each label says what it | ||
| 397 | + // wants, because the label is the whole of what the box shows. | ||
| 398 | + dir := createTools(t) | ||
| 399 | + | ||
| 400 | + list, err := tools.Load(Profile(), dir) | ||
| 401 | + if err != nil { | ||
| 402 | + t.Fatalf("tools.Load() error = %v", err) | ||
| 403 | + } | ||
| 404 | + | ||
| 405 | + for _, tool := range list.Tools() { | ||
| 406 | + for _, placeholder := range tool.Placeholders() { | ||
| 407 | + if strings.TrimSpace(placeholder.Label) == "" { | ||
| 408 | + t.Errorf("tool %q has a placeholder with no label", tool.Name) | ||
| 409 | + } | ||
| 410 | + } | ||
| 411 | + } | ||
| 412 | +} | ||
| 413 | + | ||
| 414 | +// The tools reference prints the starter file's table. Turbo Python's shipped | ||
| 415 | +// five rows for a file that had six, and claimed `Alt-T` for a menu whose key | ||
| 416 | +// is `Alt-P` — both inherited from Turbo Rust by a mechanical substitution that | ||
| 417 | +// only looked at identifiers. Nothing in either repository could see it. | ||
| 418 | +// | ||
| 419 | +// So the table is read out of the page and held to the file the editor | ||
| 420 | +// actually writes, in both languages. | ||
| 421 | +func TestTheToolsReferenceMatchesTheStarterFile(t *testing.T) { | ||
| 422 | + dir := createTools(t) | ||
| 423 | + | ||
| 424 | + list, err := tools.Load(Profile(), dir) | ||
| 425 | + if err != nil { | ||
| 426 | + t.Fatalf("tools.Load() error = %v", err) | ||
| 427 | + } | ||
| 428 | + | ||
| 429 | + for _, page := range []string{"../../docs/en/reference/golo-tools.md", "../../docs/fr/reference/golo-tools.md"} { | ||
| 430 | + raw, err := os.ReadFile(page) | ||
| 431 | + if err != nil { | ||
| 432 | + t.Fatalf("reading %s: %v", page, err) | ||
| 433 | + } | ||
| 434 | + text := string(raw) | ||
| 435 | + | ||
| 436 | + for _, tool := range list.Tools() { | ||
| 437 | + if !strings.Contains(text, "| `"+tool.Name+"` |") { | ||
| 438 | + t.Errorf("%s has no row for the tool %q", page, tool.Name) | ||
| 439 | + } | ||
| 440 | + if !strings.Contains(text, "`"+tool.Command+"`") { | ||
| 441 | + t.Errorf("%s does not print the command %q", page, tool.Command) | ||
| 442 | + } | ||
| 443 | + } | ||
| 444 | + if !strings.Contains(text, "`Alt-G`") { | ||
| 445 | + t.Errorf("%s never names Alt-G, the key the Golo menu really answers to", page) | ||
| 446 | + } | ||
| 447 | + for _, stale := range []string{"`Alt-M`", "`Alt-T`, then", "`Alt-T`, puis", "`Alt-P`"} { | ||
| 448 | + if strings.Contains(text, stale) { | ||
| 449 | + t.Errorf("%s still opens the toolchain menu with %s, which belongs to another editor", page, stale) | ||
| 450 | + } | ||
| 451 | + } | ||
| 452 | + } | ||
| 453 | +} | ||
added
internal/gololang/tools.toml.tmpl +109 -0 | new file mode 100644 | ||
| @@ -0,0 +1,109 @@ | ||
| 1 | +# turbo-golo tools. | |
| 2 | +# | |
| 3 | +# Each [[tool]] becomes one line of the Golo menu, in the order they appear | |
| 4 | +# here. name is what the menu shows; a letter between tildes is its hot key, and | |
| 5 | +# no two tools should claim the same one. | |
| 6 | +# | |
| 7 | +# command goes to "sh -c", so pipes, globs and && work: one entry can be a | |
| 8 | +# whole sequence. | |
| 9 | +# | |
| 10 | +# menu says which menu it appears in. Leave it out and the tool goes into the | |
| 11 | +# Golo menu; name anything else and that menu is created for you, in the order | |
| 12 | +# the names first appear here. A tool that has nothing to do with Golo belongs | |
| 13 | +# in one of your own: | |
| 14 | +# | |
| 15 | +# [[tool]] | |
| 16 | +# name = "~E~cho" | |
| 17 | +# command = "echo TADA" | |
| 18 | +# menu = "Tools" | |
| 19 | +# | |
| 20 | +# A {{label}} in a command is a value the editor asks for before running it, in | |
| 21 | +# a box titled after the tool. The text between the braces is what it asks for: | |
| 22 | +# | |
| 23 | +# [[tool]] | |
| 24 | +# name = "~R~un" | |
| 25 | +# command = "golo {{script}}" | |
| 26 | +# | |
| 27 | +# The value is quoted, so a path with a space in it stays one argument. Add ... | |
| 28 | +# inside the braces when you mean several arguments rather than one value: | |
| 29 | +# | |
| 30 | +# [[tool]] | |
| 31 | +# name = "Run with ~a~rguments" | |
| 32 | +# command = "golo main.golo {{arguments...}}" | |
| 33 | +# | |
| 34 | +# Double braces, not single. Single ones appear in real commands — awk '{print | |
| 35 | +# $1}' and find . -exec rm {} + are both ordinary things to put here — and | |
| 36 | +# neither is asking you for anything. | |
| 37 | +# | |
| 38 | +# output says where what the command prints goes: | |
| 39 | +# popup a dialog that fills in as it runs, and says the exit code (default) | |
| 40 | +# terminal a terminal window, for anything that reads the keyboard or runs long | |
| 41 | +# editor an editing window once it has finished, to search with Ctrl-F | |
| 42 | +# | |
| 43 | +# Commands run in the directory the editor was started in, which is why they | |
| 44 | +# see the whole project when you start from its root. Golo has no project | |
| 45 | +# manifest: a script is a file, and every command here names the file it | |
| 46 | +# works on. | |
| 47 | + | |
| 48 | +[[tool]] | |
| 49 | +name = "~R~un" | |
| 50 | +# The interpreter, on the file you name. A terminal, not a popup: a script that | |
| 51 | +# reads the keyboard has to be able to be answered, and one that runs long has | |
| 52 | +# to be able to be interrupted. | |
| 53 | +command = "golo {{script, e.g. main.golo}}" | |
| 54 | +output = "terminal" | |
| 55 | + | |
| 56 | +[[tool]] | |
| 57 | +name = "~T~est" | |
| 58 | +# Every *_test.golo under the current directory, with gololang.Testing. | |
| 59 | +command = "golo --test" | |
| 60 | +output = "popup" | |
| 61 | + | |
| 62 | +[[tool]] | |
| 63 | +name = "Test ~o~ne" | |
| 64 | +command = "golo --test {{test file or directory}}" | |
| 65 | +output = "popup" | |
| 66 | + | |
| 67 | +[[tool]] | |
| 68 | +name = "~D~ebug" | |
| 69 | +# The same interpreter with its step debugger on. It reads the keyboard, so it | |
| 70 | +# needs a terminal. | |
| 71 | +command = "golo --debug {{script, e.g. main.golo}}" | |
| 72 | +output = "terminal" | |
| 73 | + | |
| 74 | +[[tool]] | |
| 75 | +name = "R~E~PL" | |
| 76 | +# golo with no file starts its read-eval-print loop. | |
| 77 | +command = "golo" | |
| 78 | +output = "terminal" | |
| 79 | + | |
| 80 | +[[tool]] | |
| 81 | +name = "~N~ew script" | |
| 82 | +# Writes a starter program from GoloScript's own template. | |
| 83 | +command = "golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}" | |
| 84 | +output = "popup" | |
| 85 | + | |
| 86 | +[[tool]] | |
| 87 | +name = "~B~uild native" | |
| 88 | +# gogolo transpiles the script to Go and compiles it to a native executable. | |
| 89 | +# It needs the Go toolchain on PATH. | |
| 90 | +command = "gogolo build -o {{output executable}} {{script, e.g. main.golo}}" | |
| 91 | +output = "popup" | |
| 92 | + | |
| 93 | +[[tool]] | |
| 94 | +name = "Build ~w~asm" | |
| 95 | +# wagolo transpiles the script to Go and compiles it to WebAssembly with | |
| 96 | +# TinyGo. wasi is the target a runtime such as wasmtime or Node runs; js and | |
| 97 | +# wasip2 are the others. | |
| 98 | +command = "wagolo build -target={{target: wasi, js or wasip2}} -o {{output .wasm}} {{script, e.g. main.golo}}" | |
| 99 | +output = "popup" | |
| 100 | + | |
| 101 | +# A tool naming a `menu` gets a menu of its own on the bar. Nothing above does, | |
| 102 | +# so every tool above is in the Golo menu. This one is in a menu called Tools, | |
| 103 | +# which appears between Golo and Help — that is the whole mechanism. | |
| 104 | + | |
| 105 | +[[tool]] | |
| 106 | +name = "~E~cho" | |
| 107 | +command = "echo 🎉 tada!" | |
| 108 | +menu = "Tools" | |
| 109 | +output = "terminal" | |
| new file mode 100644 | |||
| @@ -0,0 +1,109 @@ | |||
| 1 | +# turbo-golo tools. | ||
| 2 | +# | ||
| 3 | +# Each [[tool]] becomes one line of the Golo menu, in the order they appear | ||
| 4 | +# here. name is what the menu shows; a letter between tildes is its hot key, and | ||
| 5 | +# no two tools should claim the same one. | ||
| 6 | +# | ||
| 7 | +# command goes to "sh -c", so pipes, globs and && work: one entry can be a | ||
| 8 | +# whole sequence. | ||
| 9 | +# | ||
| 10 | +# menu says which menu it appears in. Leave it out and the tool goes into the | ||
| 11 | +# Golo menu; name anything else and that menu is created for you, in the order | ||
| 12 | +# the names first appear here. A tool that has nothing to do with Golo belongs | ||
| 13 | +# in one of your own: | ||
| 14 | +# | ||
| 15 | +# [[tool]] | ||
| 16 | +# name = "~E~cho" | ||
| 17 | +# command = "echo TADA" | ||
| 18 | +# menu = "Tools" | ||
| 19 | +# | ||
| 20 | +# A {{label}} in a command is a value the editor asks for before running it, in | ||
| 21 | +# a box titled after the tool. The text between the braces is what it asks for: | ||
| 22 | +# | ||
| 23 | +# [[tool]] | ||
| 24 | +# name = "~R~un" | ||
| 25 | +# command = "golo {{script}}" | ||
| 26 | +# | ||
| 27 | +# The value is quoted, so a path with a space in it stays one argument. Add ... | ||
| 28 | +# inside the braces when you mean several arguments rather than one value: | ||
| 29 | +# | ||
| 30 | +# [[tool]] | ||
| 31 | +# name = "Run with ~a~rguments" | ||
| 32 | +# command = "golo main.golo {{arguments...}}" | ||
| 33 | +# | ||
| 34 | +# Double braces, not single. Single ones appear in real commands — awk '{print | ||
| 35 | +# $1}' and find . -exec rm {} + are both ordinary things to put here — and | ||
| 36 | +# neither is asking you for anything. | ||
| 37 | +# | ||
| 38 | +# output says where what the command prints goes: | ||
| 39 | +# popup a dialog that fills in as it runs, and says the exit code (default) | ||
| 40 | +# terminal a terminal window, for anything that reads the keyboard or runs long | ||
| 41 | +# editor an editing window once it has finished, to search with Ctrl-F | ||
| 42 | +# | ||
| 43 | +# Commands run in the directory the editor was started in, which is why they | ||
| 44 | +# see the whole project when you start from its root. Golo has no project | ||
| 45 | +# manifest: a script is a file, and every command here names the file it | ||
| 46 | +# works on. | ||
| 47 | + | ||
| 48 | +[[tool]] | ||
| 49 | +name = "~R~un" | ||
| 50 | +# The interpreter, on the file you name. A terminal, not a popup: a script that | ||
| 51 | +# reads the keyboard has to be able to be answered, and one that runs long has | ||
| 52 | +# to be able to be interrupted. | ||
| 53 | +command = "golo {{script, e.g. main.golo}}" | ||
| 54 | +output = "terminal" | ||
| 55 | + | ||
| 56 | +[[tool]] | ||
| 57 | +name = "~T~est" | ||
| 58 | +# Every *_test.golo under the current directory, with gololang.Testing. | ||
| 59 | +command = "golo --test" | ||
| 60 | +output = "popup" | ||
| 61 | + | ||
| 62 | +[[tool]] | ||
| 63 | +name = "Test ~o~ne" | ||
| 64 | +command = "golo --test {{test file or directory}}" | ||
| 65 | +output = "popup" | ||
| 66 | + | ||
| 67 | +[[tool]] | ||
| 68 | +name = "~D~ebug" | ||
| 69 | +# The same interpreter with its step debugger on. It reads the keyboard, so it | ||
| 70 | +# needs a terminal. | ||
| 71 | +command = "golo --debug {{script, e.g. main.golo}}" | ||
| 72 | +output = "terminal" | ||
| 73 | + | ||
| 74 | +[[tool]] | ||
| 75 | +name = "R~E~PL" | ||
| 76 | +# golo with no file starts its read-eval-print loop. | ||
| 77 | +command = "golo" | ||
| 78 | +output = "terminal" | ||
| 79 | + | ||
| 80 | +[[tool]] | ||
| 81 | +name = "~N~ew script" | ||
| 82 | +# Writes a starter program from GoloScript's own template. | ||
| 83 | +command = "golo new main --module {{module name, e.g. hello.World}} --name {{file name without .golo}}" | ||
| 84 | +output = "popup" | ||
| 85 | + | ||
| 86 | +[[tool]] | ||
| 87 | +name = "~B~uild native" | ||
| 88 | +# gogolo transpiles the script to Go and compiles it to a native executable. | ||
| 89 | +# It needs the Go toolchain on PATH. | ||
| 90 | +command = "gogolo build -o {{output executable}} {{script, e.g. main.golo}}" | ||
| 91 | +output = "popup" | ||
| 92 | + | ||
| 93 | +[[tool]] | ||
| 94 | +name = "Build ~w~asm" | ||
| 95 | +# wagolo transpiles the script to Go and compiles it to WebAssembly with | ||
| 96 | +# TinyGo. wasi is the target a runtime such as wasmtime or Node runs; js and | ||
| 97 | +# wasip2 are the others. | ||
| 98 | +command = "wagolo build -target={{target: wasi, js or wasip2}} -o {{output .wasm}} {{script, e.g. main.golo}}" | ||
| 99 | +output = "popup" | ||
| 100 | + | ||
| 101 | +# A tool naming a `menu` gets a menu of its own on the bar. Nothing above does, | ||
| 102 | +# so every tool above is in the Golo menu. This one is in a menu called Tools, | ||
| 103 | +# which appears between Golo and Help — that is the whole mechanism. | ||
| 104 | + | ||
| 105 | +[[tool]] | ||
| 106 | +name = "~E~cho" | ||
| 107 | +command = "echo 🎉 tada!" | ||
| 108 | +menu = "Tools" | ||
| 109 | +output = "terminal" | ||
added
internal/gololang/words.go +348 -0 | new file mode 100644 | ||
| @@ -0,0 +1,348 @@ | ||
| 1 | +package gololang | |
| 2 | + | |
| 3 | +// Numbers and words: what a run of digits or letters turns out to be. | |
| 4 | + | |
| 5 | +import ( | |
| 6 | + "strings" | |
| 7 | + "unicode" | |
| 8 | + | |
| 9 | + "rickub.com/turbo-editors/turbo-core/syntax" | |
| 10 | +) | |
| 11 | + | |
| 12 | +// --- numbers ---------------------------------------------------------------- | |
| 13 | + | |
| 14 | +// takeNumber colours a numeric literal the way lexer.go's readNumber reads | |
| 15 | +// one: digits, then a point and more digits if a digit follows the point, then | |
| 16 | +// an exponent with an optional sign, then the L that makes a long or the F or | |
| 17 | +// f that makes a float. | |
| 18 | +// | |
| 19 | +// The point is only part of the number when a digit follows it. That is what | |
| 20 | +// keeps 1..3 a number and a range rather than the double 1. and a stray .3, | |
| 21 | +// and it is the lexer's own test — `l.ch == '.' && isDigit(l.peekChar())`. | |
| 22 | +// | |
| 23 | +// There is no hexadecimal, no binary, no octal and no digit separator, because | |
| 24 | +// the lexer has none: 0xFF is the number 0 followed by the name xFF, and 1_000 | |
| 25 | +// is 1 followed by the name _000. Colouring either as one number would be | |
| 26 | +// inventing a literal the interpreter will reject. | |
| 27 | +// | |
| 28 | +// The whole literal is one span, so every step here advances the scanner | |
| 29 | +// without colouring and the single Emit at the end covers what they consumed. | |
| 30 | +func takeNumber(s *syntax.LineScanner) { | |
| 31 | + start := s.Pos() | |
| 32 | + advanceWhile(s, syntax.IsDigit) | |
| 33 | + | |
| 34 | + if s.Peek(0) == '.' && syntax.IsDigit(s.Peek(1)) { | |
| 35 | + s.Advance(1) | |
| 36 | + advanceWhile(s, syntax.IsDigit) | |
| 37 | + } | |
| 38 | + takeExponent(s) | |
| 39 | + takeNumberSuffix(s) | |
| 40 | + | |
| 41 | + s.Emit(start, s.Pos(), syntax.ClassNumber) | |
| 42 | +} | |
| 43 | + | |
| 44 | +// takeExponent consumes e or E, an optional sign, and the digits after them. | |
| 45 | +// | |
| 46 | +// The digits may be none. The lexer reads 1e as a float and leaves the parser | |
| 47 | +// to complain, and this scanner colours what the lexer reads. | |
| 48 | +func takeExponent(s *syntax.LineScanner) { | |
| 49 | + if s.Peek(0) != 'e' && s.Peek(0) != 'E' { | |
| 50 | + return | |
| 51 | + } | |
| 52 | + s.Advance(1) | |
| 53 | + if s.Peek(0) == '+' || s.Peek(0) == '-' { | |
| 54 | + s.Advance(1) | |
| 55 | + } | |
| 56 | + advanceWhile(s, syntax.IsDigit) | |
| 57 | +} | |
| 58 | + | |
| 59 | +// takeNumberSuffix consumes the L of a long and the F or f of a float, in | |
| 60 | +// that order, when the literal ends in them. 42L is a long; 3.14F and 2.0f are | |
| 61 | +// floats; the lexer accepts both cases of the F and only the upper case of | |
| 62 | +// the L. | |
| 63 | +func takeNumberSuffix(s *syntax.LineScanner) { | |
| 64 | + if s.Peek(0) == 'L' { | |
| 65 | + s.Advance(1) | |
| 66 | + } | |
| 67 | + if s.Peek(0) == 'F' || s.Peek(0) == 'f' { | |
| 68 | + s.Advance(1) | |
| 69 | + } | |
| 70 | +} | |
| 71 | + | |
| 72 | +// advanceWhile steps over runes that match, without colouring any of them. It | |
| 73 | +// is what a construct emitted as a single span uses in place of TakeWhile, | |
| 74 | +// which would colour each run it consumed and leave the Emit overlapping it. | |
| 75 | +func advanceWhile(s *syntax.LineScanner, matches func(rune) bool) { | |
| 76 | + for !s.AtEnd() && matches(s.Peek(0)) { | |
| 77 | + s.Advance(1) | |
| 78 | + } | |
| 79 | +} | |
| 80 | + | |
| 81 | +// --- words ------------------------------------------------------------------ | |
| 82 | + | |
| 83 | +// isIdentifierStart reports whether a rune may begin a name, by the lexer's | |
| 84 | +// own isLetter: any Unicode letter or mark, an underscore, or an emoji. | |
| 85 | +// | |
| 86 | +// This is wider than turbo-core's ASCII IsLetter on purpose. Golo lets you | |
| 87 | +// write `let 😀 = 1` and `function 🚀launch = { … }`, and a scanner that left | |
| 88 | +// those uncoloured would be telling a reader they are not names when the | |
| 89 | +// interpreter says they are. | |
| 90 | +func isIdentifierStart(r rune) bool { | |
| 91 | + return unicode.IsLetter(r) || unicode.IsMark(r) || r == '_' || isEmoji(r) | |
| 92 | +} | |
| 93 | + | |
| 94 | +// isWordRune reports whether a rune may continue a name: whatever may begin | |
| 95 | +// one, or a digit. | |
| 96 | +func isWordRune(r rune) bool { | |
| 97 | + return isIdentifierStart(r) || syntax.IsDigit(r) | |
| 98 | +} | |
| 99 | + | |
| 100 | +// emojiBlocks are the four Unicode blocks the lexer admits into a name, each | |
| 101 | +// as its first and last rune. They are the lexer's own, copied rather than | |
| 102 | +// widened: the general symbol blocks are left out there because they hold the | |
| 103 | +// operators. | |
| 104 | +var emojiBlocks = [][2]rune{ | |
| 105 | + {0x1F600, 0x1F64F}, // emoticons | |
| 106 | + {0x1F300, 0x1F5FF}, // miscellaneous symbols and pictographs | |
| 107 | + {0x1F680, 0x1F6FF}, // transport and map symbols | |
| 108 | + {0x1F900, 0x1F9FF}, // supplemental symbols and pictographs | |
| 109 | +} | |
| 110 | + | |
| 111 | +// isEmoji reports whether a rune is in one of the blocks the lexer admits into | |
| 112 | +// a name. | |
| 113 | +func isEmoji(r rune) bool { | |
| 114 | + for _, block := range emojiBlocks { | |
| 115 | + if r >= block[0] && r <= block[1] { | |
| 116 | + return true | |
| 117 | + } | |
| 118 | + } | |
| 119 | + return false | |
| 120 | +} | |
| 121 | + | |
| 122 | +// takeWord colours a name, deciding what kind of thing it is from the word | |
| 123 | +// itself and from the rune that follows it — and, for two keywords, colours | |
| 124 | +// the name that follows *them*, because that name means something only there. | |
| 125 | +func takeWord(s *syntax.LineScanner) { | |
| 126 | + start := s.Pos() | |
| 127 | + advanceWhile(s, isWordRune) | |
| 128 | + word := wordAt(s, start) | |
| 129 | + s.Emit(start, s.Pos(), classOfWord(word, s.Peek(0))) | |
| 130 | + | |
| 131 | + switch word { | |
| 132 | + case "module", "import": | |
| 133 | + takeModulePath(s) | |
| 134 | + case "function": | |
| 135 | + takeDeclaredFunctionName(s) | |
| 136 | + } | |
| 137 | +} | |
| 138 | + | |
| 139 | +// takeModulePath colours the dotted name after module or import as one span: | |
| 140 | +// hello.World, gololang.Errors, java.util.List. | |
| 141 | +// | |
| 142 | +// It is one span because it is one name — a module has no parts a program can | |
| 143 | +// take apart — and ClassType is the nearest of the seventeen classes: a module | |
| 144 | +// path names a thing rather than holding a value, and the reading it has to be | |
| 145 | +// saved from is the one where gololang.Errors looks like a variable called | |
| 146 | +// gololang with something done to it. | |
| 147 | +// | |
| 148 | +// A dot is only taken when a name follows it, so `import a.` at the end of a | |
| 149 | +// half-typed line stops before the dot and leaves it as punctuation. | |
| 150 | +func takeModulePath(s *syntax.LineScanner) { | |
| 151 | + s.SkipSpaces() | |
| 152 | + if !isIdentifierStart(s.Peek(0)) { | |
| 153 | + return | |
| 154 | + } | |
| 155 | + | |
| 156 | + start := s.Pos() | |
| 157 | + advanceWhile(s, isWordRune) | |
| 158 | + for s.Peek(0) == '.' && isIdentifierStart(s.Peek(1)) { | |
| 159 | + s.Advance(1) | |
| 160 | + advanceWhile(s, isWordRune) | |
| 161 | + } | |
| 162 | + s.Emit(start, s.Pos(), syntax.ClassType) | |
| 163 | +} | |
| 164 | + | |
| 165 | +// takeDeclaredFunctionName colours the name after the function keyword as a | |
| 166 | +// function. | |
| 167 | +// | |
| 168 | +// Everywhere else a name is a function because a parenthesis follows it, and | |
| 169 | +// a declaration is the one place that is not true: `function main = |args|` | |
| 170 | +// has the name followed by an equals sign. Without this, every function a | |
| 171 | +// file declares would be coloured as an ordinary variable at the one place a | |
| 172 | +// reader looks for it. | |
| 173 | +func takeDeclaredFunctionName(s *syntax.LineScanner) { | |
| 174 | + s.SkipSpaces() | |
| 175 | + if !isIdentifierStart(s.Peek(0)) { | |
| 176 | + return | |
| 177 | + } | |
| 178 | + | |
| 179 | + start := s.Pos() | |
| 180 | + advanceWhile(s, isWordRune) | |
| 181 | + s.Emit(start, s.Pos(), syntax.ClassFunction) | |
| 182 | +} | |
| 183 | + | |
| 184 | +// wordAt returns the word running from start to the scanner's position. | |
| 185 | +func wordAt(s *syntax.LineScanner, start int) string { | |
| 186 | + var b strings.Builder | |
| 187 | + for at := start; at < s.Pos(); at++ { | |
| 188 | + b.WriteRune(s.Peek(at - s.Pos())) | |
| 189 | + } | |
| 190 | + return b.String() | |
| 191 | +} | |
| 192 | + | |
| 193 | +// classOfWord decides what a word is, given the rune that follows it. | |
| 194 | +// | |
| 195 | +// The order is the design. A word the language names is what the language | |
| 196 | +// says it is: a keyword, one of the three literal constants, or one of the | |
| 197 | +// interpreter's built-in functions. After that comes the case rule, which in | |
| 198 | +// Golo is a convention rather than a lexical fact: structs, unions and their | |
| 199 | +// variants are capitalised by everybody — Point, Shape, Circle, Some, None — | |
| 200 | +// and nothing else customarily is, so a capitalised word is coloured as a | |
| 201 | +// type. A lower-case word followed by a parenthesis is a call, and anything | |
| 202 | +// else is a name. | |
| 203 | +// | |
| 204 | +// What the case rule costs is that a variant's constructor is coloured as a | |
| 205 | +// type — Circle(1.0) and Result_Failure("no") look like types applied to | |
| 206 | +// arguments — and a capitalised variable, which Golo permits, is coloured as | |
| 207 | +// one too. Nothing in the syntax separates them, and inventing a separation | |
| 208 | +// would mean being wrong in both directions instead of one. | |
| 209 | +func classOfWord(word string, next rune) syntax.Class { | |
| 210 | + if class, known := knownWords[word]; known { | |
| 211 | + return class | |
| 212 | + } | |
| 213 | + if startsUpperCase(word) { | |
| 214 | + return syntax.ClassType | |
| 215 | + } | |
| 216 | + if next == '(' { | |
| 217 | + return syntax.ClassFunction | |
| 218 | + } | |
| 219 | + return syntax.ClassIdentifier | |
| 220 | +} | |
| 221 | + | |
| 222 | +// startsUpperCase reports whether a word begins with an ASCII capital, which | |
| 223 | +// is what the convention means by a type's name. | |
| 224 | +func startsUpperCase(word string) bool { | |
| 225 | + return word != "" && word[0] >= 'A' && word[0] <= 'Z' | |
| 226 | +} | |
| 227 | + | |
| 228 | +// knownWords is every word the language itself names, and what each one is. | |
| 229 | +// | |
| 230 | +// It is one table rather than three because it answers one question. The three | |
| 231 | +// groups below are kept apart only so that each can carry the reasoning that | |
| 232 | +// belongs to it. | |
| 233 | +var knownWords = merge( | |
| 234 | + classify(syntax.ClassKeyword, keywords), | |
| 235 | + classify(syntax.ClassConstant, constants), | |
| 236 | + classify(syntax.ClassBuiltin, builtinFunctions), | |
| 237 | +) | |
| 238 | + | |
| 239 | +// keywords are the words Golo reserves, taken from token/token.go's keyword | |
| 240 | +// table in GoloScript — every entry of it except the three literal values, | |
| 241 | +// which are constants below. | |
| 242 | +// | |
| 243 | +// The word operators are here as keywords: and, or, not, is, isnt, oftype and | |
| 244 | +// orIfNull are reserved words that happen to compute something, and a reader | |
| 245 | +// meets them as words. There is no `then`-less if or `elseif`; `else if` is | |
| 246 | +// two keywords. | |
| 247 | +var keywords = words( | |
| 248 | + "and", "augment", "augmentation", "await", "break", "case", "catch", | |
| 249 | + "continue", "else", "finally", "for", "foreach", "function", "if", | |
| 250 | + "import", "in", "is", "isnt", "let", "local", "match", "module", "not", | |
| 251 | + "oftype", "or", "orIfNull", "otherwise", "return", "spawn", "struct", | |
| 252 | + "then", "throw", "try", "union", "var", "when", "while", "with", | |
| 253 | +) | |
| 254 | + | |
| 255 | +// constants are the values a reader meets as the language's own. They are | |
| 256 | +// keywords to the lexer and values to the reader, and every editor in this | |
| 257 | +// family colours them as constants. | |
| 258 | +var constants = words("true", "false", "null") | |
| 259 | + | |
| 260 | +// builtinFunctions are the functions the interpreter provides without an | |
| 261 | +// import, read out of evaluator.BuiltinNames() in GoloScript rather than | |
| 262 | +// remembered — 162 names, less the five prefixed with a double underscore, | |
| 263 | +// which are the test runner's own counters and which the language server | |
| 264 | +// likewise keeps out of its completion list. | |
| 265 | +// | |
| 266 | +// Some, None, Ok and Err are deliberately not here. In Golo they are not built | |
| 267 | +// in: they are the variants of ordinary unions declared in gololang.Errors, | |
| 268 | +// available only after `import gololang.Errors`, so they take the colour every | |
| 269 | +// other capitalised name does. DynamicObject *is* here, capital and all, | |
| 270 | +// because it is a builtin function that happens to be spelt like a type. | |
| 271 | +var builtinFunctions = words( | |
| 272 | + "DynamicObject", "abs", "appendFile", "array", "chanClose", "chanReceive", | |
| 273 | + "chanSend", "channel", "currentAbsDir", "currentDir", "currentTime", | |
| 274 | + "currentTimeMillis", "currentTimeNano", "dateString", "dateTimeString", | |
| 275 | + "deleteFile", "escapeJSON", "execCombinedOutput", "execCommand", | |
| 276 | + "fileExists", "fileInfo", "float", "formatTime", "fromJSON", "getenv", | |
| 277 | + "head", "httpDelete", "httpDeleteStream", "httpGet", "httpGetStream", | |
| 278 | + "httpPost", "httpPostStream", "httpPut", "httpPutStream", "httpServe", | |
| 279 | + "httpStop", "information", "int", "isNotNull", "isNull", "len", "length", | |
| 280 | + "list", "listDir", "map", "mcpAddResource", "mcpAddTool", "mcpCallTool", | |
| 281 | + "mcpConnectHTTP", "mcpConnectStdio", "mcpCreateServer", "mcpDisconnect", | |
| 282 | + "mcpListResources", "mcpListTools", "mcpReadResource", "mcpRunHTTP", | |
| 283 | + "mcpRunStdio", "mcpStopServer", "mkDir", "mutex", "mutexLock", | |
| 284 | + "mutexUnlock", "now", "observable", "observableFilter", "observableGet", | |
| 285 | + "observableMap", "observableOnChange", "observableSet", | |
| 286 | + "openAIChatCompletion", "openAIChatCompletionStream", | |
| 287 | + "openAICreateEmbedding", "openAINewClient", "parseTime", "pow", "print", | |
| 288 | + "println", "push", "raise", "range", "read", "readFile", "readln", | |
| 289 | + "require", "requireNotNull", "set", "setenv", "sharedGet", "sharedSet", | |
| 290 | + "sharedState", "sharedUpdate", "sleep", "sqrt", "str", "tail", "template", | |
| 291 | + "timeString", "toJSON", "tuiClick", "tuiComponentIds", "tuiDisplayWidth", | |
| 292 | + "tuiEmit", "tuiFocus", "tuiFrame", "tuiGetProp", "tuiHasComponent", | |
| 293 | + "tuiHide", "tuiIsFocused", "tuiIsVisible", "tuiLoad", "tuiLoadStyle", | |
| 294 | + "tuiNew", "tuiOff", "tuiOn", "tuiQuit", "tuiRenderMarkdown", "tuiRun", | |
| 295 | + "tuiSetProp", "tuiShow", "tuiSize", "tuiWheel", "tuiZoneOf", | |
| 296 | + "tupleFromArray", "type", "uiConfirm", "uiError", "uiGetColorCode", | |
| 297 | + "uiInfo", "uiMarkdownRender", "uiMarkdownStreamAppend", | |
| 298 | + "uiMarkdownStreamEnd", "uiMarkdownStreamStart", "uiPrint", | |
| 299 | + "uiPrintMultiStyle", "uiPrintln", "uiPrompt", "uiPromptMultiline", | |
| 300 | + "uiPromptPassword", "uiSpinnerError", "uiSpinnerNew", "uiSpinnerSetFrames", | |
| 301 | + "uiSpinnerSetPrefix", "uiSpinnerSetSuffix", "uiSpinnerStart", | |
| 302 | + "uiSpinnerStop", "uiSpinnerSuccess", "uiSuccess", "uiWarning", "vector", | |
| 303 | + "wasmCallNumbers", "wasmCallString", "wasmClose", "wasmHasFunction", | |
| 304 | + "wasmLoad", "wasmRegisterStringHandler", "wasmShutdown", "writeFile", | |
| 305 | +) | |
| 306 | + | |
| 307 | +// Builtins returns the interpreter's built-in function names, sorted, so a | |
| 308 | +// test can hold the table above to what a real golo answers. | |
| 309 | +func Builtins() []string { | |
| 310 | + out := make([]string, len(builtinFunctions)) | |
| 311 | + copy(out, builtinFunctions) | |
| 312 | + return out | |
| 313 | +} | |
| 314 | + | |
| 315 | +// Keywords returns the reserved words the scanner colours as keywords, so a | |
| 316 | +// test can hold the table above to what a real golo reserves. | |
| 317 | +func Keywords() []string { | |
| 318 | + out := make([]string, len(keywords)) | |
| 319 | + copy(out, keywords) | |
| 320 | + return out | |
| 321 | +} | |
| 322 | + | |
| 323 | +// words gathers a group of them, which reads better at the call sites above | |
| 324 | +// than a slice literal does. | |
| 325 | +func words(list ...string) []string { return list } | |
| 326 | + | |
| 327 | +// classify pairs every word in a group with the class it belongs to. | |
| 328 | +func classify(class syntax.Class, list []string) map[string]syntax.Class { | |
| 329 | + out := make(map[string]syntax.Class, len(list)) | |
| 330 | + for _, word := range list { | |
| 331 | + out[word] = class | |
| 332 | + } | |
| 333 | + return out | |
| 334 | +} | |
| 335 | + | |
| 336 | +// merge folds the groups into one table. An earlier group wins a word a later | |
| 337 | +// one repeats, which is what keeps a keyword a keyword. | |
| 338 | +func merge(groups ...map[string]syntax.Class) map[string]syntax.Class { | |
| 339 | + out := map[string]syntax.Class{} | |
| 340 | + for _, group := range groups { | |
| 341 | + for word, class := range group { | |
| 342 | + if _, taken := out[word]; !taken { | |
| 343 | + out[word] = class | |
| 344 | + } | |
| 345 | + } | |
| 346 | + } | |
| 347 | + return out | |
| 348 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,348 @@ | |||
| 1 | +package gololang | ||
| 2 | + | ||
| 3 | +// Numbers and words: what a run of digits or letters turns out to be. | ||
| 4 | + | ||
| 5 | +import ( | ||
| 6 | + "strings" | ||
| 7 | + "unicode" | ||
| 8 | + | ||
| 9 | + "rickub.com/turbo-editors/turbo-core/syntax" | ||
| 10 | +) | ||
| 11 | + | ||
| 12 | +// --- numbers ---------------------------------------------------------------- | ||
| 13 | + | ||
| 14 | +// takeNumber colours a numeric literal the way lexer.go's readNumber reads | ||
| 15 | +// one: digits, then a point and more digits if a digit follows the point, then | ||
| 16 | +// an exponent with an optional sign, then the L that makes a long or the F or | ||
| 17 | +// f that makes a float. | ||
| 18 | +// | ||
| 19 | +// The point is only part of the number when a digit follows it. That is what | ||
| 20 | +// keeps 1..3 a number and a range rather than the double 1. and a stray .3, | ||
| 21 | +// and it is the lexer's own test — `l.ch == '.' && isDigit(l.peekChar())`. | ||
| 22 | +// | ||
| 23 | +// There is no hexadecimal, no binary, no octal and no digit separator, because | ||
| 24 | +// the lexer has none: 0xFF is the number 0 followed by the name xFF, and 1_000 | ||
| 25 | +// is 1 followed by the name _000. Colouring either as one number would be | ||
| 26 | +// inventing a literal the interpreter will reject. | ||
| 27 | +// | ||
| 28 | +// The whole literal is one span, so every step here advances the scanner | ||
| 29 | +// without colouring and the single Emit at the end covers what they consumed. | ||
| 30 | +func takeNumber(s *syntax.LineScanner) { | ||
| 31 | + start := s.Pos() | ||
| 32 | + advanceWhile(s, syntax.IsDigit) | ||
| 33 | + | ||
| 34 | + if s.Peek(0) == '.' && syntax.IsDigit(s.Peek(1)) { | ||
| 35 | + s.Advance(1) | ||
| 36 | + advanceWhile(s, syntax.IsDigit) | ||
| 37 | + } | ||
| 38 | + takeExponent(s) | ||
| 39 | + takeNumberSuffix(s) | ||
| 40 | + | ||
| 41 | + s.Emit(start, s.Pos(), syntax.ClassNumber) | ||
| 42 | +} | ||
| 43 | + | ||
| 44 | +// takeExponent consumes e or E, an optional sign, and the digits after them. | ||
| 45 | +// | ||
| 46 | +// The digits may be none. The lexer reads 1e as a float and leaves the parser | ||
| 47 | +// to complain, and this scanner colours what the lexer reads. | ||
| 48 | +func takeExponent(s *syntax.LineScanner) { | ||
| 49 | + if s.Peek(0) != 'e' && s.Peek(0) != 'E' { | ||
| 50 | + return | ||
| 51 | + } | ||
| 52 | + s.Advance(1) | ||
| 53 | + if s.Peek(0) == '+' || s.Peek(0) == '-' { | ||
| 54 | + s.Advance(1) | ||
| 55 | + } | ||
| 56 | + advanceWhile(s, syntax.IsDigit) | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +// takeNumberSuffix consumes the L of a long and the F or f of a float, in | ||
| 60 | +// that order, when the literal ends in them. 42L is a long; 3.14F and 2.0f are | ||
| 61 | +// floats; the lexer accepts both cases of the F and only the upper case of | ||
| 62 | +// the L. | ||
| 63 | +func takeNumberSuffix(s *syntax.LineScanner) { | ||
| 64 | + if s.Peek(0) == 'L' { | ||
| 65 | + s.Advance(1) | ||
| 66 | + } | ||
| 67 | + if s.Peek(0) == 'F' || s.Peek(0) == 'f' { | ||
| 68 | + s.Advance(1) | ||
| 69 | + } | ||
| 70 | +} | ||
| 71 | + | ||
| 72 | +// advanceWhile steps over runes that match, without colouring any of them. It | ||
| 73 | +// is what a construct emitted as a single span uses in place of TakeWhile, | ||
| 74 | +// which would colour each run it consumed and leave the Emit overlapping it. | ||
| 75 | +func advanceWhile(s *syntax.LineScanner, matches func(rune) bool) { | ||
| 76 | + for !s.AtEnd() && matches(s.Peek(0)) { | ||
| 77 | + s.Advance(1) | ||
| 78 | + } | ||
| 79 | +} | ||
| 80 | + | ||
| 81 | +// --- words ------------------------------------------------------------------ | ||
| 82 | + | ||
| 83 | +// isIdentifierStart reports whether a rune may begin a name, by the lexer's | ||
| 84 | +// own isLetter: any Unicode letter or mark, an underscore, or an emoji. | ||
| 85 | +// | ||
| 86 | +// This is wider than turbo-core's ASCII IsLetter on purpose. Golo lets you | ||
| 87 | +// write `let 😀 = 1` and `function 🚀launch = { … }`, and a scanner that left | ||
| 88 | +// those uncoloured would be telling a reader they are not names when the | ||
| 89 | +// interpreter says they are. | ||
| 90 | +func isIdentifierStart(r rune) bool { | ||
| 91 | + return unicode.IsLetter(r) || unicode.IsMark(r) || r == '_' || isEmoji(r) | ||
| 92 | +} | ||
| 93 | + | ||
| 94 | +// isWordRune reports whether a rune may continue a name: whatever may begin | ||
| 95 | +// one, or a digit. | ||
| 96 | +func isWordRune(r rune) bool { | ||
| 97 | + return isIdentifierStart(r) || syntax.IsDigit(r) | ||
| 98 | +} | ||
| 99 | + | ||
| 100 | +// emojiBlocks are the four Unicode blocks the lexer admits into a name, each | ||
| 101 | +// as its first and last rune. They are the lexer's own, copied rather than | ||
| 102 | +// widened: the general symbol blocks are left out there because they hold the | ||
| 103 | +// operators. | ||
| 104 | +var emojiBlocks = [][2]rune{ | ||
| 105 | + {0x1F600, 0x1F64F}, // emoticons | ||
| 106 | + {0x1F300, 0x1F5FF}, // miscellaneous symbols and pictographs | ||
| 107 | + {0x1F680, 0x1F6FF}, // transport and map symbols | ||
| 108 | + {0x1F900, 0x1F9FF}, // supplemental symbols and pictographs | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +// isEmoji reports whether a rune is in one of the blocks the lexer admits into | ||
| 112 | +// a name. | ||
| 113 | +func isEmoji(r rune) bool { | ||
| 114 | + for _, block := range emojiBlocks { | ||
| 115 | + if r >= block[0] && r <= block[1] { | ||
| 116 | + return true | ||
| 117 | + } | ||
| 118 | + } | ||
| 119 | + return false | ||
| 120 | +} | ||
| 121 | + | ||
| 122 | +// takeWord colours a name, deciding what kind of thing it is from the word | ||
| 123 | +// itself and from the rune that follows it — and, for two keywords, colours | ||
| 124 | +// the name that follows *them*, because that name means something only there. | ||
| 125 | +func takeWord(s *syntax.LineScanner) { | ||
| 126 | + start := s.Pos() | ||
| 127 | + advanceWhile(s, isWordRune) | ||
| 128 | + word := wordAt(s, start) | ||
| 129 | + s.Emit(start, s.Pos(), classOfWord(word, s.Peek(0))) | ||
| 130 | + | ||
| 131 | + switch word { | ||
| 132 | + case "module", "import": | ||
| 133 | + takeModulePath(s) | ||
| 134 | + case "function": | ||
| 135 | + takeDeclaredFunctionName(s) | ||
| 136 | + } | ||
| 137 | +} | ||
| 138 | + | ||
| 139 | +// takeModulePath colours the dotted name after module or import as one span: | ||
| 140 | +// hello.World, gololang.Errors, java.util.List. | ||
| 141 | +// | ||
| 142 | +// It is one span because it is one name — a module has no parts a program can | ||
| 143 | +// take apart — and ClassType is the nearest of the seventeen classes: a module | ||
| 144 | +// path names a thing rather than holding a value, and the reading it has to be | ||
| 145 | +// saved from is the one where gololang.Errors looks like a variable called | ||
| 146 | +// gololang with something done to it. | ||
| 147 | +// | ||
| 148 | +// A dot is only taken when a name follows it, so `import a.` at the end of a | ||
| 149 | +// half-typed line stops before the dot and leaves it as punctuation. | ||
| 150 | +func takeModulePath(s *syntax.LineScanner) { | ||
| 151 | + s.SkipSpaces() | ||
| 152 | + if !isIdentifierStart(s.Peek(0)) { | ||
| 153 | + return | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + start := s.Pos() | ||
| 157 | + advanceWhile(s, isWordRune) | ||
| 158 | + for s.Peek(0) == '.' && isIdentifierStart(s.Peek(1)) { | ||
| 159 | + s.Advance(1) | ||
| 160 | + advanceWhile(s, isWordRune) | ||
| 161 | + } | ||
| 162 | + s.Emit(start, s.Pos(), syntax.ClassType) | ||
| 163 | +} | ||
| 164 | + | ||
| 165 | +// takeDeclaredFunctionName colours the name after the function keyword as a | ||
| 166 | +// function. | ||
| 167 | +// | ||
| 168 | +// Everywhere else a name is a function because a parenthesis follows it, and | ||
| 169 | +// a declaration is the one place that is not true: `function main = |args|` | ||
| 170 | +// has the name followed by an equals sign. Without this, every function a | ||
| 171 | +// file declares would be coloured as an ordinary variable at the one place a | ||
| 172 | +// reader looks for it. | ||
| 173 | +func takeDeclaredFunctionName(s *syntax.LineScanner) { | ||
| 174 | + s.SkipSpaces() | ||
| 175 | + if !isIdentifierStart(s.Peek(0)) { | ||
| 176 | + return | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + start := s.Pos() | ||
| 180 | + advanceWhile(s, isWordRune) | ||
| 181 | + s.Emit(start, s.Pos(), syntax.ClassFunction) | ||
| 182 | +} | ||
| 183 | + | ||
| 184 | +// wordAt returns the word running from start to the scanner's position. | ||
| 185 | +func wordAt(s *syntax.LineScanner, start int) string { | ||
| 186 | + var b strings.Builder | ||
| 187 | + for at := start; at < s.Pos(); at++ { | ||
| 188 | + b.WriteRune(s.Peek(at - s.Pos())) | ||
| 189 | + } | ||
| 190 | + return b.String() | ||
| 191 | +} | ||
| 192 | + | ||
| 193 | +// classOfWord decides what a word is, given the rune that follows it. | ||
| 194 | +// | ||
| 195 | +// The order is the design. A word the language names is what the language | ||
| 196 | +// says it is: a keyword, one of the three literal constants, or one of the | ||
| 197 | +// interpreter's built-in functions. After that comes the case rule, which in | ||
| 198 | +// Golo is a convention rather than a lexical fact: structs, unions and their | ||
| 199 | +// variants are capitalised by everybody — Point, Shape, Circle, Some, None — | ||
| 200 | +// and nothing else customarily is, so a capitalised word is coloured as a | ||
| 201 | +// type. A lower-case word followed by a parenthesis is a call, and anything | ||
| 202 | +// else is a name. | ||
| 203 | +// | ||
| 204 | +// What the case rule costs is that a variant's constructor is coloured as a | ||
| 205 | +// type — Circle(1.0) and Result_Failure("no") look like types applied to | ||
| 206 | +// arguments — and a capitalised variable, which Golo permits, is coloured as | ||
| 207 | +// one too. Nothing in the syntax separates them, and inventing a separation | ||
| 208 | +// would mean being wrong in both directions instead of one. | ||
| 209 | +func classOfWord(word string, next rune) syntax.Class { | ||
| 210 | + if class, known := knownWords[word]; known { | ||
| 211 | + return class | ||
| 212 | + } | ||
| 213 | + if startsUpperCase(word) { | ||
| 214 | + return syntax.ClassType | ||
| 215 | + } | ||
| 216 | + if next == '(' { | ||
| 217 | + return syntax.ClassFunction | ||
| 218 | + } | ||
| 219 | + return syntax.ClassIdentifier | ||
| 220 | +} | ||
| 221 | + | ||
| 222 | +// startsUpperCase reports whether a word begins with an ASCII capital, which | ||
| 223 | +// is what the convention means by a type's name. | ||
| 224 | +func startsUpperCase(word string) bool { | ||
| 225 | + return word != "" && word[0] >= 'A' && word[0] <= 'Z' | ||
| 226 | +} | ||
| 227 | + | ||
| 228 | +// knownWords is every word the language itself names, and what each one is. | ||
| 229 | +// | ||
| 230 | +// It is one table rather than three because it answers one question. The three | ||
| 231 | +// groups below are kept apart only so that each can carry the reasoning that | ||
| 232 | +// belongs to it. | ||
| 233 | +var knownWords = merge( | ||
| 234 | + classify(syntax.ClassKeyword, keywords), | ||
| 235 | + classify(syntax.ClassConstant, constants), | ||
| 236 | + classify(syntax.ClassBuiltin, builtinFunctions), | ||
| 237 | +) | ||
| 238 | + | ||
| 239 | +// keywords are the words Golo reserves, taken from token/token.go's keyword | ||
| 240 | +// table in GoloScript — every entry of it except the three literal values, | ||
| 241 | +// which are constants below. | ||
| 242 | +// | ||
| 243 | +// The word operators are here as keywords: and, or, not, is, isnt, oftype and | ||
| 244 | +// orIfNull are reserved words that happen to compute something, and a reader | ||
| 245 | +// meets them as words. There is no `then`-less if or `elseif`; `else if` is | ||
| 246 | +// two keywords. | ||
| 247 | +var keywords = words( | ||
| 248 | + "and", "augment", "augmentation", "await", "break", "case", "catch", | ||
| 249 | + "continue", "else", "finally", "for", "foreach", "function", "if", | ||
| 250 | + "import", "in", "is", "isnt", "let", "local", "match", "module", "not", | ||
| 251 | + "oftype", "or", "orIfNull", "otherwise", "return", "spawn", "struct", | ||
| 252 | + "then", "throw", "try", "union", "var", "when", "while", "with", | ||
| 253 | +) | ||
| 254 | + | ||
| 255 | +// constants are the values a reader meets as the language's own. They are | ||
| 256 | +// keywords to the lexer and values to the reader, and every editor in this | ||
| 257 | +// family colours them as constants. | ||
| 258 | +var constants = words("true", "false", "null") | ||
| 259 | + | ||
| 260 | +// builtinFunctions are the functions the interpreter provides without an | ||
| 261 | +// import, read out of evaluator.BuiltinNames() in GoloScript rather than | ||
| 262 | +// remembered — 162 names, less the five prefixed with a double underscore, | ||
| 263 | +// which are the test runner's own counters and which the language server | ||
| 264 | +// likewise keeps out of its completion list. | ||
| 265 | +// | ||
| 266 | +// Some, None, Ok and Err are deliberately not here. In Golo they are not built | ||
| 267 | +// in: they are the variants of ordinary unions declared in gololang.Errors, | ||
| 268 | +// available only after `import gololang.Errors`, so they take the colour every | ||
| 269 | +// other capitalised name does. DynamicObject *is* here, capital and all, | ||
| 270 | +// because it is a builtin function that happens to be spelt like a type. | ||
| 271 | +var builtinFunctions = words( | ||
| 272 | + "DynamicObject", "abs", "appendFile", "array", "chanClose", "chanReceive", | ||
| 273 | + "chanSend", "channel", "currentAbsDir", "currentDir", "currentTime", | ||
| 274 | + "currentTimeMillis", "currentTimeNano", "dateString", "dateTimeString", | ||
| 275 | + "deleteFile", "escapeJSON", "execCombinedOutput", "execCommand", | ||
| 276 | + "fileExists", "fileInfo", "float", "formatTime", "fromJSON", "getenv", | ||
| 277 | + "head", "httpDelete", "httpDeleteStream", "httpGet", "httpGetStream", | ||
| 278 | + "httpPost", "httpPostStream", "httpPut", "httpPutStream", "httpServe", | ||
| 279 | + "httpStop", "information", "int", "isNotNull", "isNull", "len", "length", | ||
| 280 | + "list", "listDir", "map", "mcpAddResource", "mcpAddTool", "mcpCallTool", | ||
| 281 | + "mcpConnectHTTP", "mcpConnectStdio", "mcpCreateServer", "mcpDisconnect", | ||
| 282 | + "mcpListResources", "mcpListTools", "mcpReadResource", "mcpRunHTTP", | ||
| 283 | + "mcpRunStdio", "mcpStopServer", "mkDir", "mutex", "mutexLock", | ||
| 284 | + "mutexUnlock", "now", "observable", "observableFilter", "observableGet", | ||
| 285 | + "observableMap", "observableOnChange", "observableSet", | ||
| 286 | + "openAIChatCompletion", "openAIChatCompletionStream", | ||
| 287 | + "openAICreateEmbedding", "openAINewClient", "parseTime", "pow", "print", | ||
| 288 | + "println", "push", "raise", "range", "read", "readFile", "readln", | ||
| 289 | + "require", "requireNotNull", "set", "setenv", "sharedGet", "sharedSet", | ||
| 290 | + "sharedState", "sharedUpdate", "sleep", "sqrt", "str", "tail", "template", | ||
| 291 | + "timeString", "toJSON", "tuiClick", "tuiComponentIds", "tuiDisplayWidth", | ||
| 292 | + "tuiEmit", "tuiFocus", "tuiFrame", "tuiGetProp", "tuiHasComponent", | ||
| 293 | + "tuiHide", "tuiIsFocused", "tuiIsVisible", "tuiLoad", "tuiLoadStyle", | ||
| 294 | + "tuiNew", "tuiOff", "tuiOn", "tuiQuit", "tuiRenderMarkdown", "tuiRun", | ||
| 295 | + "tuiSetProp", "tuiShow", "tuiSize", "tuiWheel", "tuiZoneOf", | ||
| 296 | + "tupleFromArray", "type", "uiConfirm", "uiError", "uiGetColorCode", | ||
| 297 | + "uiInfo", "uiMarkdownRender", "uiMarkdownStreamAppend", | ||
| 298 | + "uiMarkdownStreamEnd", "uiMarkdownStreamStart", "uiPrint", | ||
| 299 | + "uiPrintMultiStyle", "uiPrintln", "uiPrompt", "uiPromptMultiline", | ||
| 300 | + "uiPromptPassword", "uiSpinnerError", "uiSpinnerNew", "uiSpinnerSetFrames", | ||
| 301 | + "uiSpinnerSetPrefix", "uiSpinnerSetSuffix", "uiSpinnerStart", | ||
| 302 | + "uiSpinnerStop", "uiSpinnerSuccess", "uiSuccess", "uiWarning", "vector", | ||
| 303 | + "wasmCallNumbers", "wasmCallString", "wasmClose", "wasmHasFunction", | ||
| 304 | + "wasmLoad", "wasmRegisterStringHandler", "wasmShutdown", "writeFile", | ||
| 305 | +) | ||
| 306 | + | ||
| 307 | +// Builtins returns the interpreter's built-in function names, sorted, so a | ||
| 308 | +// test can hold the table above to what a real golo answers. | ||
| 309 | +func Builtins() []string { | ||
| 310 | + out := make([]string, len(builtinFunctions)) | ||
| 311 | + copy(out, builtinFunctions) | ||
| 312 | + return out | ||
| 313 | +} | ||
| 314 | + | ||
| 315 | +// Keywords returns the reserved words the scanner colours as keywords, so a | ||
| 316 | +// test can hold the table above to what a real golo reserves. | ||
| 317 | +func Keywords() []string { | ||
| 318 | + out := make([]string, len(keywords)) | ||
| 319 | + copy(out, keywords) | ||
| 320 | + return out | ||
| 321 | +} | ||
| 322 | + | ||
| 323 | +// words gathers a group of them, which reads better at the call sites above | ||
| 324 | +// than a slice literal does. | ||
| 325 | +func words(list ...string) []string { return list } | ||
| 326 | + | ||
| 327 | +// classify pairs every word in a group with the class it belongs to. | ||
| 328 | +func classify(class syntax.Class, list []string) map[string]syntax.Class { | ||
| 329 | + out := make(map[string]syntax.Class, len(list)) | ||
| 330 | + for _, word := range list { | ||
| 331 | + out[word] = class | ||
| 332 | + } | ||
| 333 | + return out | ||
| 334 | +} | ||
| 335 | + | ||
| 336 | +// merge folds the groups into one table. An earlier group wins a word a later | ||
| 337 | +// one repeats, which is what keeps a keyword a keyword. | ||
| 338 | +func merge(groups ...map[string]syntax.Class) map[string]syntax.Class { | ||
| 339 | + out := map[string]syntax.Class{} | ||
| 340 | + for _, group := range groups { | ||
| 341 | + for word, class := range group { | ||
| 342 | + if _, taken := out[word]; !taken { | ||
| 343 | + out[word] = class | ||
| 344 | + } | ||
| 345 | + } | ||
| 346 | + } | ||
| 347 | + return out | ||
| 348 | +} | ||
added
main.go +204 -0 | new file mode 100644 | ||
| @@ -0,0 +1,204 @@ | ||
| 1 | +// Command turbo-golo is a Turbo C-style editor for Golo: a full-screen | |
| 2 | +// terminal IDE with menus, movable windows, syntax colouring and completion | |
| 3 | +// from golo lsp. | |
| 4 | +// | |
| 5 | +// Almost all of it is turbo-core, the library every Turbo editor is built on. | |
| 6 | +// What is here is the command line, the terminal, and internal/gololang — | |
| 7 | +// the profile that says this one is for Golo. | |
| 8 | +// | |
| 9 | +// Usage: | |
| 10 | +// | |
| 11 | +// turbo-golo [flags] [file...] | |
| 12 | +// | |
| 13 | +// Flags: | |
| 14 | +// | |
| 15 | +// -theme name the colour theme to start with, overriding the project's | |
| 16 | +// -list-themes print the available themes and exit | |
| 17 | +// -no-lsp do not start a language server | |
| 18 | +// -version print the version and exit | |
| 19 | +package main | |
| 20 | + | |
| 21 | +import ( | |
| 22 | + "context" | |
| 23 | + "errors" | |
| 24 | + "flag" | |
| 25 | + "fmt" | |
| 26 | + "os" | |
| 27 | + | |
| 28 | + "github.com/gdamore/tcell/v2" | |
| 29 | + | |
| 30 | + "rickub.com/turbo-editors/turbo-core/app" | |
| 31 | + "rickub.com/turbo-editors/turbo-core/profile" | |
| 32 | + "rickub.com/turbo-editors/turbo-core/settings" | |
| 33 | + "rickub.com/turbo-editors/turbo-core/theme" | |
| 34 | + "rickub.com/turbo-editors/turbo-core/version" | |
| 35 | + | |
| 36 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | |
| 37 | +) | |
| 38 | + | |
| 39 | +func main() { | |
| 40 | + if err := run(); err != nil { | |
| 41 | + fmt.Fprintf(os.Stderr, "%s: %v\n", gololang.Slug, err) | |
| 42 | + os.Exit(1) | |
| 43 | + } | |
| 44 | +} | |
| 45 | + | |
| 46 | +// options are what the command line asked for. | |
| 47 | +type options struct { | |
| 48 | + theme string | |
| 49 | + listThemes bool | |
| 50 | + noLSP bool | |
| 51 | + version bool | |
| 52 | + files []string | |
| 53 | +} | |
| 54 | + | |
| 55 | +// parseFlags reads the command line. | |
| 56 | +func parseFlags() options { | |
| 57 | + var opts options | |
| 58 | + | |
| 59 | + // The default is empty rather than the theme's name so that "was -theme | |
| 60 | + // given?" can still be answered afterwards, which is what lets the project | |
| 61 | + // settings fill it in without overriding an explicit choice. | |
| 62 | + flag.StringVar(&opts.theme, "theme", "", "colour theme to start with (default from the project, else "+theme.DefaultName+")") | |
| 63 | + flag.BoolVar(&opts.listThemes, "list-themes", false, "print the available themes and exit") | |
| 64 | + flag.BoolVar(&opts.noLSP, "no-lsp", false, "do not start a language server") | |
| 65 | + flag.BoolVar(&opts.version, "version", false, "print the version and exit") | |
| 66 | + flag.Parse() | |
| 67 | + | |
| 68 | + opts.files = flag.Args() | |
| 69 | + return opts | |
| 70 | +} | |
| 71 | + | |
| 72 | +// run does the work, so that main is nothing but error reporting. | |
| 73 | +func run() error { | |
| 74 | + opts := parseFlags() | |
| 75 | + // Registering here rather than from an init function is what makes "this | |
| 76 | + // editor knows Golo" a line somebody can read. | |
| 77 | + gololang.Register() | |
| 78 | + p := gololang.Profile() | |
| 79 | + | |
| 80 | + switch { | |
| 81 | + case opts.version: | |
| 82 | + fmt.Printf("%s %s\n", p.Name, version.Current()) | |
| 83 | + return nil | |
| 84 | + case opts.listThemes: | |
| 85 | + return listThemes(p) | |
| 86 | + } | |
| 87 | + | |
| 88 | + return edit(opts, p) | |
| 89 | +} | |
| 90 | + | |
| 91 | +// listThemes prints every theme that can be loaded, with its description. | |
| 92 | +func listThemes(p profile.Profile) error { | |
| 93 | + userDir := p.ThemeDir() | |
| 94 | + | |
| 95 | + for _, name := range theme.Available(userDir) { | |
| 96 | + loaded, err := theme.Load(name, userDir) | |
| 97 | + if err != nil { | |
| 98 | + fmt.Printf("%-16s (cannot be loaded: %v)\n", name, err) | |
| 99 | + continue | |
| 100 | + } | |
| 101 | + fmt.Printf("%-16s %s\n", name, loaded.Description()) | |
| 102 | + } | |
| 103 | + | |
| 104 | + if userDir != "" { | |
| 105 | + fmt.Printf("\nYour own themes go in %s\n", userDir) | |
| 106 | + } | |
| 107 | + return nil | |
| 108 | +} | |
| 109 | + | |
| 110 | +// edit opens the terminal and runs the editor until the user leaves. | |
| 111 | +func edit(opts options, p profile.Profile) error { | |
| 112 | + project, projectSettings := loadProjectSettings(p) | |
| 113 | + | |
| 114 | + screen, err := newScreen() | |
| 115 | + if err != nil { | |
| 116 | + return err | |
| 117 | + } | |
| 118 | + // The screen must be given back whatever happens, or a crash leaves the | |
| 119 | + // terminal in raw mode with no cursor. | |
| 120 | + defer screen.Fini() | |
| 121 | + | |
| 122 | + editor := app.New(screen, themeName(opts, projectSettings), p) | |
| 123 | + if settings.Exists(p, project) { | |
| 124 | + editor.UseSettings(projectSettings, settings.Path(p, project)) | |
| 125 | + } | |
| 126 | + openFiles(editor, opts.files) | |
| 127 | + | |
| 128 | + ctx, cancel := context.WithCancel(context.Background()) | |
| 129 | + defer cancel() | |
| 130 | + if !opts.noLSP { | |
| 131 | + editor.StartLanguageServer(ctx, app.ProjectRoot(p, opts.files)) | |
| 132 | + } | |
| 133 | + defer editor.Language().Stop(context.Background()) | |
| 134 | + | |
| 135 | + return editor.Run() | |
| 136 | +} | |
| 137 | + | |
| 138 | +// loadProjectSettings reads .turbo-golo/settings.toml from the working | |
| 139 | +// directory, and returns that directory along with what it found. | |
| 140 | +// | |
| 141 | +// The working directory alone is looked in, with no walk up towards the root: | |
| 142 | +// "the project" is where you started the editor, which is a rule you can hold | |
| 143 | +// in your head. A file that is there but unreadable is reported on standard | |
| 144 | +// error and then ignored — a broken settings file must not stop the editor | |
| 145 | +// opening, because the editor is how you would fix it. | |
| 146 | +func loadProjectSettings(p profile.Profile) (string, settings.Settings) { | |
| 147 | + project, err := os.Getwd() | |
| 148 | + if err != nil { | |
| 149 | + project = "." | |
| 150 | + } | |
| 151 | + | |
| 152 | + loaded, err := settings.Load(p, project) | |
| 153 | + switch { | |
| 154 | + case errors.Is(err, settings.ErrNotFound): | |
| 155 | + return project, settings.Default() | |
| 156 | + case err != nil: | |
| 157 | + fmt.Fprintf(os.Stderr, "%s: %v\n", gololang.Slug, err) | |
| 158 | + return project, settings.Default() | |
| 159 | + } | |
| 160 | + return project, loaded | |
| 161 | +} | |
| 162 | + | |
| 163 | +// themeName decides which theme to start in. | |
| 164 | +// | |
| 165 | +// A -theme flag wins, because it is the more explicit statement of the two and | |
| 166 | +// is how you try a theme without editing a file everyone shares. The project's | |
| 167 | +// settings come next, and the built-in default last. | |
| 168 | +func themeName(opts options, projectSettings settings.Settings) string { | |
| 169 | + switch { | |
| 170 | + case opts.theme != "": | |
| 171 | + return opts.theme | |
| 172 | + case projectSettings.Theme != "": | |
| 173 | + return projectSettings.Theme | |
| 174 | + default: | |
| 175 | + return theme.DefaultName | |
| 176 | + } | |
| 177 | +} | |
| 178 | + | |
| 179 | +// newScreen opens the terminal and turns on what the editor needs from it. | |
| 180 | +func newScreen() (tcell.Screen, error) { | |
| 181 | + screen, err := tcell.NewScreen() | |
| 182 | + if err != nil { | |
| 183 | + return nil, fmt.Errorf("opening the terminal: %w", err) | |
| 184 | + } | |
| 185 | + if err := screen.Init(); err != nil { | |
| 186 | + return nil, fmt.Errorf("initialising the terminal: %w", err) | |
| 187 | + } | |
| 188 | + | |
| 189 | + screen.EnableMouse() | |
| 190 | + screen.EnablePaste() | |
| 191 | + return screen, nil | |
| 192 | +} | |
| 193 | + | |
| 194 | +// openFiles opens the files named on the command line, or an empty window when | |
| 195 | +// none were. | |
| 196 | +func openFiles(editor *app.App, files []string) { | |
| 197 | + if len(files) == 0 { | |
| 198 | + editor.NewFile() | |
| 199 | + return | |
| 200 | + } | |
| 201 | + for _, file := range files { | |
| 202 | + editor.Open(file) | |
| 203 | + } | |
| 204 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,204 @@ | |||
| 1 | +// Command turbo-golo is a Turbo C-style editor for Golo: a full-screen | ||
| 2 | +// terminal IDE with menus, movable windows, syntax colouring and completion | ||
| 3 | +// from golo lsp. | ||
| 4 | +// | ||
| 5 | +// Almost all of it is turbo-core, the library every Turbo editor is built on. | ||
| 6 | +// What is here is the command line, the terminal, and internal/gololang — | ||
| 7 | +// the profile that says this one is for Golo. | ||
| 8 | +// | ||
| 9 | +// Usage: | ||
| 10 | +// | ||
| 11 | +// turbo-golo [flags] [file...] | ||
| 12 | +// | ||
| 13 | +// Flags: | ||
| 14 | +// | ||
| 15 | +// -theme name the colour theme to start with, overriding the project's | ||
| 16 | +// -list-themes print the available themes and exit | ||
| 17 | +// -no-lsp do not start a language server | ||
| 18 | +// -version print the version and exit | ||
| 19 | +package main | ||
| 20 | + | ||
| 21 | +import ( | ||
| 22 | + "context" | ||
| 23 | + "errors" | ||
| 24 | + "flag" | ||
| 25 | + "fmt" | ||
| 26 | + "os" | ||
| 27 | + | ||
| 28 | + "github.com/gdamore/tcell/v2" | ||
| 29 | + | ||
| 30 | + "rickub.com/turbo-editors/turbo-core/app" | ||
| 31 | + "rickub.com/turbo-editors/turbo-core/profile" | ||
| 32 | + "rickub.com/turbo-editors/turbo-core/settings" | ||
| 33 | + "rickub.com/turbo-editors/turbo-core/theme" | ||
| 34 | + "rickub.com/turbo-editors/turbo-core/version" | ||
| 35 | + | ||
| 36 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | ||
| 37 | +) | ||
| 38 | + | ||
| 39 | +func main() { | ||
| 40 | + if err := run(); err != nil { | ||
| 41 | + fmt.Fprintf(os.Stderr, "%s: %v\n", gololang.Slug, err) | ||
| 42 | + os.Exit(1) | ||
| 43 | + } | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +// options are what the command line asked for. | ||
| 47 | +type options struct { | ||
| 48 | + theme string | ||
| 49 | + listThemes bool | ||
| 50 | + noLSP bool | ||
| 51 | + version bool | ||
| 52 | + files []string | ||
| 53 | +} | ||
| 54 | + | ||
| 55 | +// parseFlags reads the command line. | ||
| 56 | +func parseFlags() options { | ||
| 57 | + var opts options | ||
| 58 | + | ||
| 59 | + // The default is empty rather than the theme's name so that "was -theme | ||
| 60 | + // given?" can still be answered afterwards, which is what lets the project | ||
| 61 | + // settings fill it in without overriding an explicit choice. | ||
| 62 | + flag.StringVar(&opts.theme, "theme", "", "colour theme to start with (default from the project, else "+theme.DefaultName+")") | ||
| 63 | + flag.BoolVar(&opts.listThemes, "list-themes", false, "print the available themes and exit") | ||
| 64 | + flag.BoolVar(&opts.noLSP, "no-lsp", false, "do not start a language server") | ||
| 65 | + flag.BoolVar(&opts.version, "version", false, "print the version and exit") | ||
| 66 | + flag.Parse() | ||
| 67 | + | ||
| 68 | + opts.files = flag.Args() | ||
| 69 | + return opts | ||
| 70 | +} | ||
| 71 | + | ||
| 72 | +// run does the work, so that main is nothing but error reporting. | ||
| 73 | +func run() error { | ||
| 74 | + opts := parseFlags() | ||
| 75 | + // Registering here rather than from an init function is what makes "this | ||
| 76 | + // editor knows Golo" a line somebody can read. | ||
| 77 | + gololang.Register() | ||
| 78 | + p := gololang.Profile() | ||
| 79 | + | ||
| 80 | + switch { | ||
| 81 | + case opts.version: | ||
| 82 | + fmt.Printf("%s %s\n", p.Name, version.Current()) | ||
| 83 | + return nil | ||
| 84 | + case opts.listThemes: | ||
| 85 | + return listThemes(p) | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + return edit(opts, p) | ||
| 89 | +} | ||
| 90 | + | ||
| 91 | +// listThemes prints every theme that can be loaded, with its description. | ||
| 92 | +func listThemes(p profile.Profile) error { | ||
| 93 | + userDir := p.ThemeDir() | ||
| 94 | + | ||
| 95 | + for _, name := range theme.Available(userDir) { | ||
| 96 | + loaded, err := theme.Load(name, userDir) | ||
| 97 | + if err != nil { | ||
| 98 | + fmt.Printf("%-16s (cannot be loaded: %v)\n", name, err) | ||
| 99 | + continue | ||
| 100 | + } | ||
| 101 | + fmt.Printf("%-16s %s\n", name, loaded.Description()) | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + if userDir != "" { | ||
| 105 | + fmt.Printf("\nYour own themes go in %s\n", userDir) | ||
| 106 | + } | ||
| 107 | + return nil | ||
| 108 | +} | ||
| 109 | + | ||
| 110 | +// edit opens the terminal and runs the editor until the user leaves. | ||
| 111 | +func edit(opts options, p profile.Profile) error { | ||
| 112 | + project, projectSettings := loadProjectSettings(p) | ||
| 113 | + | ||
| 114 | + screen, err := newScreen() | ||
| 115 | + if err != nil { | ||
| 116 | + return err | ||
| 117 | + } | ||
| 118 | + // The screen must be given back whatever happens, or a crash leaves the | ||
| 119 | + // terminal in raw mode with no cursor. | ||
| 120 | + defer screen.Fini() | ||
| 121 | + | ||
| 122 | + editor := app.New(screen, themeName(opts, projectSettings), p) | ||
| 123 | + if settings.Exists(p, project) { | ||
| 124 | + editor.UseSettings(projectSettings, settings.Path(p, project)) | ||
| 125 | + } | ||
| 126 | + openFiles(editor, opts.files) | ||
| 127 | + | ||
| 128 | + ctx, cancel := context.WithCancel(context.Background()) | ||
| 129 | + defer cancel() | ||
| 130 | + if !opts.noLSP { | ||
| 131 | + editor.StartLanguageServer(ctx, app.ProjectRoot(p, opts.files)) | ||
| 132 | + } | ||
| 133 | + defer editor.Language().Stop(context.Background()) | ||
| 134 | + | ||
| 135 | + return editor.Run() | ||
| 136 | +} | ||
| 137 | + | ||
| 138 | +// loadProjectSettings reads .turbo-golo/settings.toml from the working | ||
| 139 | +// directory, and returns that directory along with what it found. | ||
| 140 | +// | ||
| 141 | +// The working directory alone is looked in, with no walk up towards the root: | ||
| 142 | +// "the project" is where you started the editor, which is a rule you can hold | ||
| 143 | +// in your head. A file that is there but unreadable is reported on standard | ||
| 144 | +// error and then ignored — a broken settings file must not stop the editor | ||
| 145 | +// opening, because the editor is how you would fix it. | ||
| 146 | +func loadProjectSettings(p profile.Profile) (string, settings.Settings) { | ||
| 147 | + project, err := os.Getwd() | ||
| 148 | + if err != nil { | ||
| 149 | + project = "." | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + loaded, err := settings.Load(p, project) | ||
| 153 | + switch { | ||
| 154 | + case errors.Is(err, settings.ErrNotFound): | ||
| 155 | + return project, settings.Default() | ||
| 156 | + case err != nil: | ||
| 157 | + fmt.Fprintf(os.Stderr, "%s: %v\n", gololang.Slug, err) | ||
| 158 | + return project, settings.Default() | ||
| 159 | + } | ||
| 160 | + return project, loaded | ||
| 161 | +} | ||
| 162 | + | ||
| 163 | +// themeName decides which theme to start in. | ||
| 164 | +// | ||
| 165 | +// A -theme flag wins, because it is the more explicit statement of the two and | ||
| 166 | +// is how you try a theme without editing a file everyone shares. The project's | ||
| 167 | +// settings come next, and the built-in default last. | ||
| 168 | +func themeName(opts options, projectSettings settings.Settings) string { | ||
| 169 | + switch { | ||
| 170 | + case opts.theme != "": | ||
| 171 | + return opts.theme | ||
| 172 | + case projectSettings.Theme != "": | ||
| 173 | + return projectSettings.Theme | ||
| 174 | + default: | ||
| 175 | + return theme.DefaultName | ||
| 176 | + } | ||
| 177 | +} | ||
| 178 | + | ||
| 179 | +// newScreen opens the terminal and turns on what the editor needs from it. | ||
| 180 | +func newScreen() (tcell.Screen, error) { | ||
| 181 | + screen, err := tcell.NewScreen() | ||
| 182 | + if err != nil { | ||
| 183 | + return nil, fmt.Errorf("opening the terminal: %w", err) | ||
| 184 | + } | ||
| 185 | + if err := screen.Init(); err != nil { | ||
| 186 | + return nil, fmt.Errorf("initialising the terminal: %w", err) | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + screen.EnableMouse() | ||
| 190 | + screen.EnablePaste() | ||
| 191 | + return screen, nil | ||
| 192 | +} | ||
| 193 | + | ||
| 194 | +// openFiles opens the files named on the command line, or an empty window when | ||
| 195 | +// none were. | ||
| 196 | +func openFiles(editor *app.App, files []string) { | ||
| 197 | + if len(files) == 0 { | ||
| 198 | + editor.NewFile() | ||
| 199 | + return | ||
| 200 | + } | ||
| 201 | + for _, file := range files { | ||
| 202 | + editor.Open(file) | ||
| 203 | + } | ||
| 204 | +} | ||
added
main_test.go +159 -0 | new file mode 100644 | ||
| @@ -0,0 +1,159 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "flag" | |
| 5 | + "os" | |
| 6 | + "path/filepath" | |
| 7 | + "slices" | |
| 8 | + "testing" | |
| 9 | + | |
| 10 | + "rickub.com/turbo-editors/turbo-core/app" | |
| 11 | + "rickub.com/turbo-editors/turbo-core/settings" | |
| 12 | + | |
| 13 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | |
| 14 | +) | |
| 15 | + | |
| 16 | +// withArgs runs the command line parser against a fixed argument list, and | |
| 17 | +// restores the real one afterwards so tests do not affect each other. | |
| 18 | +func withArgs(t *testing.T, args ...string) options { | |
| 19 | + t.Helper() | |
| 20 | + | |
| 21 | + realArgs, realFlags := os.Args, flag.CommandLine | |
| 22 | + t.Cleanup(func() { os.Args, flag.CommandLine = realArgs, realFlags }) | |
| 23 | + | |
| 24 | + os.Args = append([]string{"turbo-golo"}, args...) | |
| 25 | + flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) | |
| 26 | + return parseFlags() | |
| 27 | +} | |
| 28 | + | |
| 29 | +func TestFilesAreWhatIsLeftAfterTheFlags(t *testing.T) { | |
| 30 | + opts := withArgs(t, "-no-lsp", "main.golo", "lib/x.golo") | |
| 31 | + | |
| 32 | + if !opts.noLSP { | |
| 33 | + t.Error("-no-lsp was not read") | |
| 34 | + } | |
| 35 | + if want := []string{"main.golo", "lib/x.golo"}; !slices.Equal(opts.files, want) { | |
| 36 | + t.Errorf("files = %v, want %v", opts.files, want) | |
| 37 | + } | |
| 38 | +} | |
| 39 | + | |
| 40 | +func TestTheThemeFlagDefaultsToEmptyRatherThanToAName(t *testing.T) { | |
| 41 | + // Empty is what lets "was -theme given?" still be answered afterwards, and | |
| 42 | + // that is what lets the project's settings fill it in without overriding an | |
| 43 | + // explicit choice. | |
| 44 | + if opts := withArgs(t); opts.theme != "" { | |
| 45 | + t.Errorf("theme = %q with no flag, want the empty string", opts.theme) | |
| 46 | + } | |
| 47 | +} | |
| 48 | + | |
| 49 | +func TestTheThemeFlagBeatsTheProjectSettings(t *testing.T) { | |
| 50 | + project := settings.Default() | |
| 51 | + project.Theme = "cobalt" | |
| 52 | + | |
| 53 | + if got := themeName(options{theme: "monochrome"}, project); got != "monochrome" { | |
| 54 | + t.Errorf("themeName() = %q, want the flag's %q", got, "monochrome") | |
| 55 | + } | |
| 56 | +} | |
| 57 | + | |
| 58 | +func TestTheProjectSettingsBeatTheBuiltInDefault(t *testing.T) { | |
| 59 | + project := settings.Default() | |
| 60 | + project.Theme = "cobalt" | |
| 61 | + | |
| 62 | + if got := themeName(options{}, project); got != "cobalt" { | |
| 63 | + t.Errorf("themeName() = %q, want the project's %q", got, "cobalt") | |
| 64 | + } | |
| 65 | +} | |
| 66 | + | |
| 67 | +func TestTheBuiltInDefaultIsUsedWhenNobodySaysOtherwise(t *testing.T) { | |
| 68 | + if got := themeName(options{}, settings.Default()); got == "" { | |
| 69 | + t.Error("themeName() = \"\" with nothing set, want the library's default") | |
| 70 | + } | |
| 71 | +} | |
| 72 | + | |
| 73 | +func TestTheProjectRootIsTheDirectoryOfTheFile(t *testing.T) { | |
| 74 | + // app.ProjectRoot walks up looking for the profile's RootMarkers, and Golo | |
| 75 | + // has none: a script is a file, and there is no manifest above it to find. | |
| 76 | + // So the server is started in the file's own directory, however deep. | |
| 77 | + root := t.TempDir() | |
| 78 | + nested := filepath.Join(root, "scripts", "tools") | |
| 79 | + if err := os.MkdirAll(nested, 0o755); err != nil { | |
| 80 | + t.Fatal(err) | |
| 81 | + } | |
| 82 | + | |
| 83 | + file := filepath.Join(nested, "main.golo") | |
| 84 | + if got := app.ProjectRoot(gololang.Profile(), []string{file}); got != nested { | |
| 85 | + t.Errorf("ProjectRoot(%q) = %q, want the file's own directory %q", file, got, nested) | |
| 86 | + } | |
| 87 | +} | |
| 88 | + | |
| 89 | +func TestNothingAboveTheFileChangesTheRoot(t *testing.T) { | |
| 90 | + // A .git directory and a main.golo higher up are the two things somebody | |
| 91 | + // might expect to count as a project. Neither is a marker here, so neither | |
| 92 | + // moves the root: the rule is "no walk", and this pins it. | |
| 93 | + root := t.TempDir() | |
| 94 | + nested := filepath.Join(root, "lib") | |
| 95 | + if err := os.MkdirAll(filepath.Join(root, ".git"), 0o755); err != nil { | |
| 96 | + t.Fatal(err) | |
| 97 | + } | |
| 98 | + if err := os.MkdirAll(nested, 0o755); err != nil { | |
| 99 | + t.Fatal(err) | |
| 100 | + } | |
| 101 | + if err := os.WriteFile(filepath.Join(root, "main.golo"), []byte("module m\n"), 0o644); err != nil { | |
| 102 | + t.Fatal(err) | |
| 103 | + } | |
| 104 | + | |
| 105 | + file := filepath.Join(nested, "helpers.golo") | |
| 106 | + if got := app.ProjectRoot(gololang.Profile(), []string{file}); got != nested { | |
| 107 | + t.Errorf("ProjectRoot(%q) = %q, want %q; something above the file was taken for a marker", file, got, nested) | |
| 108 | + } | |
| 109 | +} | |
| 110 | + | |
| 111 | +func TestWithNoFileTheRootIsTheWorkingDirectory(t *testing.T) { | |
| 112 | + // `turbo-golo` with no file opens an empty window, and the server has to | |
| 113 | + // be started somewhere. Where the editor was started is the only answer. | |
| 114 | + dir := t.TempDir() | |
| 115 | + t.Chdir(dir) | |
| 116 | + | |
| 117 | + got := app.ProjectRoot(gololang.Profile(), nil) | |
| 118 | + want, err := os.Getwd() | |
| 119 | + if err != nil { | |
| 120 | + t.Fatal(err) | |
| 121 | + } | |
| 122 | + if got != want { | |
| 123 | + t.Errorf("ProjectRoot() with no file = %q, want the working directory %q", got, want) | |
| 124 | + } | |
| 125 | +} | |
| 126 | + | |
| 127 | +func TestLoadingSettingsFromADirectoryWithNoneGivesTheDefaults(t *testing.T) { | |
| 128 | + // A directory somebody merely started the editor in has said nothing, and | |
| 129 | + // the editor must not write to it. The starter file turns autosave on; the | |
| 130 | + // default leaves it off. | |
| 131 | + dir := t.TempDir() | |
| 132 | + t.Chdir(dir) | |
| 133 | + | |
| 134 | + project, loaded := loadProjectSettings(gololang.Profile()) | |
| 135 | + if project == "" { | |
| 136 | + t.Error("loadProjectSettings returned no project directory") | |
| 137 | + } | |
| 138 | + if loaded.Autosave { | |
| 139 | + t.Error("autosave is on with no settings file, want it off") | |
| 140 | + } | |
| 141 | +} | |
| 142 | + | |
| 143 | +func TestABrokenSettingsFileDoesNotStopTheEditorOpening(t *testing.T) { | |
| 144 | + // A broken settings file must not stop the editor opening, because the | |
| 145 | + // editor is how you would fix it. | |
| 146 | + dir := t.TempDir() | |
| 147 | + p := gololang.Profile() | |
| 148 | + if err := os.MkdirAll(filepath.Join(dir, p.ProjectDir()), 0o755); err != nil { | |
| 149 | + t.Fatal(err) | |
| 150 | + } | |
| 151 | + if err := os.WriteFile(settings.Path(p, dir), []byte("this is not ["), 0o644); err != nil { | |
| 152 | + t.Fatal(err) | |
| 153 | + } | |
| 154 | + t.Chdir(dir) | |
| 155 | + | |
| 156 | + if _, loaded := loadProjectSettings(p); loaded != settings.Default() { | |
| 157 | + t.Errorf("loadProjectSettings() = %+v with a broken file, want the defaults", loaded) | |
| 158 | + } | |
| 159 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,159 @@ | |||
| 1 | +package main | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "flag" | ||
| 5 | + "os" | ||
| 6 | + "path/filepath" | ||
| 7 | + "slices" | ||
| 8 | + "testing" | ||
| 9 | + | ||
| 10 | + "rickub.com/turbo-editors/turbo-core/app" | ||
| 11 | + "rickub.com/turbo-editors/turbo-core/settings" | ||
| 12 | + | ||
| 13 | + "rickub.com/turbo-editors/turbo-golo/internal/gololang" | ||
| 14 | +) | ||
| 15 | + | ||
| 16 | +// withArgs runs the command line parser against a fixed argument list, and | ||
| 17 | +// restores the real one afterwards so tests do not affect each other. | ||
| 18 | +func withArgs(t *testing.T, args ...string) options { | ||
| 19 | + t.Helper() | ||
| 20 | + | ||
| 21 | + realArgs, realFlags := os.Args, flag.CommandLine | ||
| 22 | + t.Cleanup(func() { os.Args, flag.CommandLine = realArgs, realFlags }) | ||
| 23 | + | ||
| 24 | + os.Args = append([]string{"turbo-golo"}, args...) | ||
| 25 | + flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) | ||
| 26 | + return parseFlags() | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +func TestFilesAreWhatIsLeftAfterTheFlags(t *testing.T) { | ||
| 30 | + opts := withArgs(t, "-no-lsp", "main.golo", "lib/x.golo") | ||
| 31 | + | ||
| 32 | + if !opts.noLSP { | ||
| 33 | + t.Error("-no-lsp was not read") | ||
| 34 | + } | ||
| 35 | + if want := []string{"main.golo", "lib/x.golo"}; !slices.Equal(opts.files, want) { | ||
| 36 | + t.Errorf("files = %v, want %v", opts.files, want) | ||
| 37 | + } | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +func TestTheThemeFlagDefaultsToEmptyRatherThanToAName(t *testing.T) { | ||
| 41 | + // Empty is what lets "was -theme given?" still be answered afterwards, and | ||
| 42 | + // that is what lets the project's settings fill it in without overriding an | ||
| 43 | + // explicit choice. | ||
| 44 | + if opts := withArgs(t); opts.theme != "" { | ||
| 45 | + t.Errorf("theme = %q with no flag, want the empty string", opts.theme) | ||
| 46 | + } | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +func TestTheThemeFlagBeatsTheProjectSettings(t *testing.T) { | ||
| 50 | + project := settings.Default() | ||
| 51 | + project.Theme = "cobalt" | ||
| 52 | + | ||
| 53 | + if got := themeName(options{theme: "monochrome"}, project); got != "monochrome" { | ||
| 54 | + t.Errorf("themeName() = %q, want the flag's %q", got, "monochrome") | ||
| 55 | + } | ||
| 56 | +} | ||
| 57 | + | ||
| 58 | +func TestTheProjectSettingsBeatTheBuiltInDefault(t *testing.T) { | ||
| 59 | + project := settings.Default() | ||
| 60 | + project.Theme = "cobalt" | ||
| 61 | + | ||
| 62 | + if got := themeName(options{}, project); got != "cobalt" { | ||
| 63 | + t.Errorf("themeName() = %q, want the project's %q", got, "cobalt") | ||
| 64 | + } | ||
| 65 | +} | ||
| 66 | + | ||
| 67 | +func TestTheBuiltInDefaultIsUsedWhenNobodySaysOtherwise(t *testing.T) { | ||
| 68 | + if got := themeName(options{}, settings.Default()); got == "" { | ||
| 69 | + t.Error("themeName() = \"\" with nothing set, want the library's default") | ||
| 70 | + } | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +func TestTheProjectRootIsTheDirectoryOfTheFile(t *testing.T) { | ||
| 74 | + // app.ProjectRoot walks up looking for the profile's RootMarkers, and Golo | ||
| 75 | + // has none: a script is a file, and there is no manifest above it to find. | ||
| 76 | + // So the server is started in the file's own directory, however deep. | ||
| 77 | + root := t.TempDir() | ||
| 78 | + nested := filepath.Join(root, "scripts", "tools") | ||
| 79 | + if err := os.MkdirAll(nested, 0o755); err != nil { | ||
| 80 | + t.Fatal(err) | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + file := filepath.Join(nested, "main.golo") | ||
| 84 | + if got := app.ProjectRoot(gololang.Profile(), []string{file}); got != nested { | ||
| 85 | + t.Errorf("ProjectRoot(%q) = %q, want the file's own directory %q", file, got, nested) | ||
| 86 | + } | ||
| 87 | +} | ||
| 88 | + | ||
| 89 | +func TestNothingAboveTheFileChangesTheRoot(t *testing.T) { | ||
| 90 | + // A .git directory and a main.golo higher up are the two things somebody | ||
| 91 | + // might expect to count as a project. Neither is a marker here, so neither | ||
| 92 | + // moves the root: the rule is "no walk", and this pins it. | ||
| 93 | + root := t.TempDir() | ||
| 94 | + nested := filepath.Join(root, "lib") | ||
| 95 | + if err := os.MkdirAll(filepath.Join(root, ".git"), 0o755); err != nil { | ||
| 96 | + t.Fatal(err) | ||
| 97 | + } | ||
| 98 | + if err := os.MkdirAll(nested, 0o755); err != nil { | ||
| 99 | + t.Fatal(err) | ||
| 100 | + } | ||
| 101 | + if err := os.WriteFile(filepath.Join(root, "main.golo"), []byte("module m\n"), 0o644); err != nil { | ||
| 102 | + t.Fatal(err) | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + file := filepath.Join(nested, "helpers.golo") | ||
| 106 | + if got := app.ProjectRoot(gololang.Profile(), []string{file}); got != nested { | ||
| 107 | + t.Errorf("ProjectRoot(%q) = %q, want %q; something above the file was taken for a marker", file, got, nested) | ||
| 108 | + } | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +func TestWithNoFileTheRootIsTheWorkingDirectory(t *testing.T) { | ||
| 112 | + // `turbo-golo` with no file opens an empty window, and the server has to | ||
| 113 | + // be started somewhere. Where the editor was started is the only answer. | ||
| 114 | + dir := t.TempDir() | ||
| 115 | + t.Chdir(dir) | ||
| 116 | + | ||
| 117 | + got := app.ProjectRoot(gololang.Profile(), nil) | ||
| 118 | + want, err := os.Getwd() | ||
| 119 | + if err != nil { | ||
| 120 | + t.Fatal(err) | ||
| 121 | + } | ||
| 122 | + if got != want { | ||
| 123 | + t.Errorf("ProjectRoot() with no file = %q, want the working directory %q", got, want) | ||
| 124 | + } | ||
| 125 | +} | ||
| 126 | + | ||
| 127 | +func TestLoadingSettingsFromADirectoryWithNoneGivesTheDefaults(t *testing.T) { | ||
| 128 | + // A directory somebody merely started the editor in has said nothing, and | ||
| 129 | + // the editor must not write to it. The starter file turns autosave on; the | ||
| 130 | + // default leaves it off. | ||
| 131 | + dir := t.TempDir() | ||
| 132 | + t.Chdir(dir) | ||
| 133 | + | ||
| 134 | + project, loaded := loadProjectSettings(gololang.Profile()) | ||
| 135 | + if project == "" { | ||
| 136 | + t.Error("loadProjectSettings returned no project directory") | ||
| 137 | + } | ||
| 138 | + if loaded.Autosave { | ||
| 139 | + t.Error("autosave is on with no settings file, want it off") | ||
| 140 | + } | ||
| 141 | +} | ||
| 142 | + | ||
| 143 | +func TestABrokenSettingsFileDoesNotStopTheEditorOpening(t *testing.T) { | ||
| 144 | + // A broken settings file must not stop the editor opening, because the | ||
| 145 | + // editor is how you would fix it. | ||
| 146 | + dir := t.TempDir() | ||
| 147 | + p := gololang.Profile() | ||
| 148 | + if err := os.MkdirAll(filepath.Join(dir, p.ProjectDir()), 0o755); err != nil { | ||
| 149 | + t.Fatal(err) | ||
| 150 | + } | ||
| 151 | + if err := os.WriteFile(settings.Path(p, dir), []byte("this is not ["), 0o644); err != nil { | ||
| 152 | + t.Fatal(err) | ||
| 153 | + } | ||
| 154 | + t.Chdir(dir) | ||
| 155 | + | ||
| 156 | + if _, loaded := loadProjectSettings(p); loaded != settings.Default() { | ||
| 157 | + t.Errorf("loadProjectSettings() = %+v with a broken file, want the defaults", loaded) | ||
| 158 | + } | ||
| 159 | +} | ||
added
new.branch.sh +3 -0 | new file mode 100755 | ||
| @@ -0,0 +1,3 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +git switch -c "$1" | |
| 3 | +git push -u origin "$1" | |
| new file mode 100755 | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | +#!/bin/bash | ||
| 2 | +git switch -c "$1" | ||
| 3 | +git push -u origin "$1" | ||
added
new.feature.sh +6 -0 | new file mode 100755 | ||
| @@ -0,0 +1,6 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +git switch -c feature/"$1" | |
| 3 | +# touch new.feature.txt | |
| 4 | +# git add . | |
| 5 | +# git commit -m "$1" | |
| 6 | +git push -u origin feature/"$1" | |
| new file mode 100755 | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | +#!/bin/bash | ||
| 2 | +git switch -c feature/"$1" | ||
| 3 | +# touch new.feature.txt | ||
| 4 | +# git add . | ||
| 5 | +# git commit -m "$1" | ||
| 6 | +git push -u origin feature/"$1" | ||
added
new.fix.sh +6 -0 | new file mode 100755 | ||
| @@ -0,0 +1,6 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +git switch -c fix/"$1" | |
| 3 | +#touch new.fix.txt | |
| 4 | +# git add . | |
| 5 | +# git commit -m "$1" | |
| 6 | +git push -u origin fix/"$1" | |
| new file mode 100755 | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | +#!/bin/bash | ||
| 2 | +git switch -c fix/"$1" | ||
| 3 | +#touch new.fix.txt | ||
| 4 | +# git add . | ||
| 5 | +# git commit -m "$1" | ||
| 6 | +git push -u origin fix/"$1" | ||
added
release_test.go +594 -0 | new file mode 100644 | ||
| @@ -0,0 +1,594 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "os" | |
| 5 | + "os/exec" | |
| 6 | + "path/filepath" | |
| 7 | + "runtime" | |
| 8 | + "strings" | |
| 9 | + "testing" | |
| 10 | +) | |
| 11 | + | |
| 12 | +// readReleaseScript returns the release builder, so its rules can be asserted | |
| 13 | +// without running it: running it cross-compiles five binaries, which is not a | |
| 14 | +// unit test. (Running the tagging script, on the other hand, is done below, | |
| 15 | +// against a throwaway clone.) | |
| 16 | +func readReleaseScript(t *testing.T) string { | |
| 17 | + t.Helper() | |
| 18 | + | |
| 19 | + script, err := os.ReadFile("02-build-releases.sh") | |
| 20 | + if err != nil { | |
| 21 | + t.Fatalf("cannot read the release script: %v", err) | |
| 22 | + } | |
| 23 | + return string(script) | |
| 24 | +} | |
| 25 | + | |
| 26 | +func TestTheReleaseScriptStampsTheBinariesItShips(t *testing.T) { | |
| 27 | + // Without -ldflags on the cross-compile, every downloaded binary reports | |
| 28 | + // "devel" while the release page names a version. The host binary would | |
| 29 | + // still be right, so nothing but this notices. | |
| 30 | + script := readReleaseScript(t) | |
| 31 | + | |
| 32 | + build := commandContaining(t, script, "GOARCH=") | |
| 33 | + if !strings.Contains(build, "-ldflags") { | |
| 34 | + t.Errorf("the cross-compile does not stamp a version:\n%s", build) | |
| 35 | + } | |
| 36 | +} | |
| 37 | + | |
| 38 | +func TestTheReleaseScriptTakesTheStampFromTheMakefile(t *testing.T) { | |
| 39 | + // Repeating the -X paths in the script is how the host binary and the | |
| 40 | + // downloads would come to disagree about which package holds the version. | |
| 41 | + script := readReleaseScript(t) | |
| 42 | + | |
| 43 | + if !strings.Contains(script, "make --no-print-directory ldflags") { | |
| 44 | + t.Error("the script does not read the linker flags from the Makefile") | |
| 45 | + } | |
| 46 | + if strings.Contains(script, "version.stamp=") { | |
| 47 | + t.Error("the script spells out the -X path, which the Makefile already owns") | |
| 48 | + } | |
| 49 | +} | |
| 50 | + | |
| 51 | +func TestTheReleaseScriptStampsTheTagItIsReleasing(t *testing.T) { | |
| 52 | + // The release *is* ${TAG}, so that is what the binaries say. Letting the | |
| 53 | + // Makefile's default stand would stamp `git describe`, which answers a | |
| 54 | + // different question — where HEAD is — and disagrees the moment anyone | |
| 55 | + // commits after tagging. | |
| 56 | + script := readReleaseScript(t) | |
| 57 | + | |
| 58 | + flags := commandContaining(t, script, "ldflags") | |
| 59 | + if !strings.Contains(flags, `VERSION="${TAG}"`) { | |
| 60 | + t.Errorf("the stamp does not come from TAG:\n%s", flags) | |
| 61 | + } | |
| 62 | + if build := commandContaining(t, script, "make build"); !strings.Contains(build, `VERSION="${TAG}"`) { | |
| 63 | + t.Errorf("the host build carries a different version from the assets:\n%s", build) | |
| 64 | + } | |
| 65 | +} | |
| 66 | + | |
| 67 | +func TestTheReleaseScriptDoesNotParseTheVersionOutOfProse(t *testing.T) { | |
| 68 | + // `-version` is written for a person and has changed shape once already; | |
| 69 | + // awk '{print $NF}' on it read a timestamp and failed a release. | |
| 70 | + script := readReleaseScript(t) | |
| 71 | + | |
| 72 | + if strings.Contains(script, "$NF") { | |
| 73 | + t.Error("the script reads a field out of the -version line, which is prose") | |
| 74 | + } | |
| 75 | +} | |
| 76 | + | |
| 77 | +func TestTheMakefileHandsOutTheFlagsThatStampABuild(t *testing.T) { | |
| 78 | + // The contract the release script depends on: `make ldflags` prints flags | |
| 79 | + // that actually put *the Makefile's own version* into a binary. | |
| 80 | + // | |
| 81 | + // It is checked against `make version` rather than against "not devel", | |
| 82 | + // because a checkout with no tags — a fresh clone, or a repository that has | |
| 83 | + // never had a release — correctly reports devel, and a test that called | |
| 84 | + // that a failure would be testing the tags rather than the flags. | |
| 85 | + version, err := exec.Command("make", "--no-print-directory", "version").Output() | |
| 86 | + if err != nil { | |
| 87 | + t.Fatalf("make version: %v", err) | |
| 88 | + } | |
| 89 | + // internal/version drops the leading v of a tag, so the comparison has to | |
| 90 | + // as well: `make version` says v0.2.1 and the binary says 0.2.1. | |
| 91 | + number := strings.TrimPrefix(strings.Fields(strings.TrimSpace(string(version)))[0], "v") | |
| 92 | + | |
| 93 | + flags, err := exec.Command("make", "--no-print-directory", "ldflags").Output() | |
| 94 | + if err != nil { | |
| 95 | + t.Fatalf("make ldflags: %v", err) | |
| 96 | + } | |
| 97 | + | |
| 98 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | |
| 99 | + build := exec.Command("go", "build", "-trimpath", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".") | |
| 100 | + build.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH) | |
| 101 | + if out, err := build.CombinedOutput(); err != nil { | |
| 102 | + t.Fatalf("building with those flags failed: %v\n%s", err, out) | |
| 103 | + } | |
| 104 | + | |
| 105 | + reported, err := exec.Command(binary, "-version").Output() | |
| 106 | + if err != nil { | |
| 107 | + t.Fatalf("the stamped binary does not run: %v", err) | |
| 108 | + } | |
| 109 | + if !strings.Contains(string(reported), number) { | |
| 110 | + t.Errorf("-version printed %q, want it to carry the Makefile's version %q", reported, number) | |
| 111 | + } | |
| 112 | + if strings.Contains(string(reported), "unknown") { | |
| 113 | + t.Errorf("-version printed %q, so nothing reached the linker at all", reported) | |
| 114 | + } | |
| 115 | +} | |
| 116 | + | |
| 117 | +func TestMakeLdflagsTakesTheVersionItIsGiven(t *testing.T) { | |
| 118 | + // The release script overrides VERSION with the tag it is releasing, and | |
| 119 | + // everything downstream rests on that override reaching the linker. | |
| 120 | + flags, err := exec.Command("make", "--no-print-directory", "ldflags", "VERSION=v9.9.9").Output() | |
| 121 | + if err != nil { | |
| 122 | + t.Fatalf("make ldflags: %v", err) | |
| 123 | + } | |
| 124 | + | |
| 125 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | |
| 126 | + build := exec.Command("go", "build", "-trimpath", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".") | |
| 127 | + build.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH) | |
| 128 | + if out, err := build.CombinedOutput(); err != nil { | |
| 129 | + t.Fatalf("building with those flags failed: %v\n%s", err, out) | |
| 130 | + } | |
| 131 | + | |
| 132 | + reported, err := exec.Command(binary, "-version").Output() | |
| 133 | + if err != nil { | |
| 134 | + t.Fatalf("the stamped binary does not run: %v", err) | |
| 135 | + } | |
| 136 | + if !strings.Contains(string(reported), "9.9.9") { | |
| 137 | + t.Errorf("-version printed %q, so VERSION=v9.9.9 never reached the linker", reported) | |
| 138 | + } | |
| 139 | +} | |
| 140 | + | |
| 141 | +// commandContaining returns the first shell command of a script holding a | |
| 142 | +// fragment, with backslash continuations joined: a command's flags are often | |
| 143 | +// on the line after the one that names it, and a test about the command should | |
| 144 | +// not depend on where it happens to wrap. | |
| 145 | +func commandContaining(t *testing.T, script, fragment string) string { | |
| 146 | + t.Helper() | |
| 147 | + | |
| 148 | + joined := strings.ReplaceAll(script, "\\\n", " ") | |
| 149 | + for _, line := range strings.Split(joined, "\n") { | |
| 150 | + if strings.Contains(line, fragment) { | |
| 151 | + return strings.TrimSpace(line) | |
| 152 | + } | |
| 153 | + } | |
| 154 | + t.Fatalf("no command in the script contains %q", fragment) | |
| 155 | + return "" | |
| 156 | +} | |
| 157 | + | |
| 158 | +// readTagScript returns the tagging script, whose failure modes are what the | |
| 159 | +// release builder is left to notice when they are not caught here. | |
| 160 | +func readTagScript(t *testing.T) string { | |
| 161 | + t.Helper() | |
| 162 | + | |
| 163 | + script, err := os.ReadFile("01-release.tag.sh") | |
| 164 | + if err != nil { | |
| 165 | + t.Fatalf("cannot read the tagging script: %v", err) | |
| 166 | + } | |
| 167 | + return string(script) | |
| 168 | +} | |
| 169 | + | |
| 170 | +func TestTheTagScriptStopsOnTheFirstFailure(t *testing.T) { | |
| 171 | + // Without this, `git tag` refusing a tag that already existed was skipped | |
| 172 | + // in silence and the `git push` after it pushed the OLD tag, cutting a | |
| 173 | + // release from a commit nobody meant. | |
| 174 | + if !strings.Contains(readTagScript(t), "set -euo pipefail") { | |
| 175 | + t.Error("the tagging script does not stop on a failing step") | |
| 176 | + } | |
| 177 | +} | |
| 178 | + | |
| 179 | +func TestTheTagScriptRefusesATagThatAlreadyExists(t *testing.T) { | |
| 180 | + script := readTagScript(t) | |
| 181 | + | |
| 182 | + for _, want := range []string{ | |
| 183 | + "git rev-parse -q --verify", // taken locally | |
| 184 | + "git ls-remote --tags origin", // taken on the remote, after a local delete | |
| 185 | + } { | |
| 186 | + if !strings.Contains(script, want) { | |
| 187 | + t.Errorf("the tagging script never checks %q", want) | |
| 188 | + } | |
| 189 | + } | |
| 190 | +} | |
| 191 | + | |
| 192 | +func TestTheTagScriptSurvivesHavingNothingToCommit(t *testing.T) { | |
| 193 | + // Under `set -e` a plain `git commit` with a clean tree ends the release, | |
| 194 | + // which is wrong: the work being already committed is the normal case on a | |
| 195 | + // second run. | |
| 196 | + script := readTagScript(t) | |
| 197 | + | |
| 198 | + if !strings.Contains(script, "git diff --cached --quiet") { | |
| 199 | + t.Error("the tagging script commits without checking there is anything to commit") | |
| 200 | + } | |
| 201 | +} | |
| 202 | + | |
| 203 | +func TestTheTagScriptTagsOnlyAfterThePushSucceeded(t *testing.T) { | |
| 204 | + // A tag left behind pointing at a commit the remote has never seen is the | |
| 205 | + // state that needs a force push to escape. | |
| 206 | + script := readTagScript(t) | |
| 207 | + | |
| 208 | + push := strings.Index(script, `git push origin "$(git rev-parse`) | |
| 209 | + tag := strings.Index(script, `git tag -a "${TAG}"`) | |
| 210 | + if push < 0 || tag < 0 { | |
| 211 | + t.Fatal("the tagging script no longer pushes and tags") | |
| 212 | + } | |
| 213 | + if tag < push { | |
| 214 | + t.Error("the script tags before pushing, so a rejected push leaves a stray tag") | |
| 215 | + } | |
| 216 | +} | |
| 217 | + | |
| 218 | +// skipInsideARelease stops a test that runs the tagging script from running | |
| 219 | +// while the tagging script is running it. | |
| 220 | +// | |
| 221 | +// The script sets this before `make check`, and `make check` runs this suite. | |
| 222 | +// Without the guard the two call each other forever — which is not a test-only | |
| 223 | +// hazard: a real release would recurse in exactly the same way. The Release | |
| 224 | +// workflow sets it too, for the same reason. | |
| 225 | +func skipInsideARelease(t *testing.T) { | |
| 226 | + t.Helper() | |
| 227 | + if os.Getenv("TURBO_GOLO_RELEASING") != "" { | |
| 228 | + t.Skip("running inside a release; not starting another one") | |
| 229 | + } | |
| 230 | +} | |
| 231 | + | |
| 232 | +func TestTheTagScriptRunsTheSuiteBeforePublishing(t *testing.T) { | |
| 233 | + // A version people will download, and the proxy will cache, is the wrong | |
| 234 | + // place to find out the suite was red. | |
| 235 | + if !strings.Contains(readTagScript(t), "make --no-print-directory check") { | |
| 236 | + t.Error("the tagging script publishes without running make check") | |
| 237 | + } | |
| 238 | +} | |
| 239 | + | |
| 240 | +func TestTheTagScriptRefusesAReplaceDirective(t *testing.T) { | |
| 241 | + // The proxy serves go.mod as written, so `go install …@TAG` on a module | |
| 242 | + // carrying a replace looks for turbo-core in a directory that does not | |
| 243 | + // exist on the installer's machine. | |
| 244 | + if !strings.Contains(readTagScript(t), "replace") { | |
| 245 | + t.Error("the tagging script does not check go.mod for a replace directive") | |
| 246 | + } | |
| 247 | +} | |
| 248 | + | |
| 249 | +func TestThisModuleHasNoReplaceDirective(t *testing.T) { | |
| 250 | + // The check above only helps if it is true today as well. | |
| 251 | + data, err := os.ReadFile("go.mod") | |
| 252 | + if err != nil { | |
| 253 | + t.Fatalf("reading go.mod: %v", err) | |
| 254 | + } | |
| 255 | + for _, line := range strings.Split(string(data), "\n") { | |
| 256 | + if strings.HasPrefix(strings.TrimSpace(line), "replace ") { | |
| 257 | + t.Errorf("go.mod carries %q; a published module must not", line) | |
| 258 | + } | |
| 259 | + } | |
| 260 | +} | |
| 261 | + | |
| 262 | +func TestTheReleaseToolingNeedsNoPersonalToken(t *testing.T) { | |
| 263 | + // The Release workflow publishes with the job's own GITHUB_TOKEN, which is | |
| 264 | + // the only credential Rickub's release API accepts. A script still reading | |
| 265 | + // a token file is a credential that cannot work and has to be kept | |
| 266 | + // somewhere all the same — and a 02 or 04 left in the tree is a second | |
| 267 | + // pipeline somebody will run by mistake. | |
| 268 | + for _, script := range []string{"01-release.tag.sh", "02-build-releases.sh"} { | |
| 269 | + data, err := os.ReadFile(script) | |
| 270 | + if err != nil { | |
| 271 | + t.Fatalf("reading %s: %v", script, err) | |
| 272 | + } | |
| 273 | + for _, secret := range []string{"token.env", "${TOKEN}"} { | |
| 274 | + if strings.Contains(string(data), secret) { | |
| 275 | + t.Errorf("%s still reads %s", script, secret) | |
| 276 | + } | |
| 277 | + } | |
| 278 | + } | |
| 279 | + for _, gone := range []string{"02-release.publish.sh", "04-release.upload-binaries.sh"} { | |
| 280 | + if _, err := os.Stat(gone); err == nil { | |
| 281 | + t.Errorf("%s is still there; the workflow publishes and attaches the binaries now", gone) | |
| 282 | + } | |
| 283 | + } | |
| 284 | +} | |
| 285 | + | |
| 286 | +func TestTheTagScriptTagsAndPushesForReal(t *testing.T) { | |
| 287 | + // The whole flow, in a throwaway clone with its own bare remote, so no tag | |
| 288 | + // is ever created in the real repository. Reading the script is not the | |
| 289 | + // same as running it: every guard above was added because one of them was | |
| 290 | + // wrong once. | |
| 291 | + skipInsideARelease(t) | |
| 292 | + if _, err := exec.LookPath("git"); err != nil { | |
| 293 | + t.Skip("git is not available") | |
| 294 | + } | |
| 295 | + | |
| 296 | + remote, clone := throwawayClone(t) | |
| 297 | + | |
| 298 | + out, err := runAllowingFailure(t, clone, "./01-release.tag.sh") | |
| 299 | + if err != nil { | |
| 300 | + t.Fatalf("the tagging script failed:\n%s", out) | |
| 301 | + } | |
| 302 | + if !strings.Contains(out, "published") { | |
| 303 | + t.Errorf("the script did not report publishing:\n%s", out) | |
| 304 | + } | |
| 305 | + | |
| 306 | + tags, _ := runAllowingFailure(t, remote, "git", "tag") | |
| 307 | + if !strings.Contains(tags, "v0.0.1-test") { | |
| 308 | + t.Errorf("the remote has tags %q, want v0.0.1-test", strings.TrimSpace(tags)) | |
| 309 | + } | |
| 310 | +} | |
| 311 | + | |
| 312 | +func TestTheTagScriptRefusesATagItAlreadyPublished(t *testing.T) { | |
| 313 | + // Moving a published version is not an option: the proxy caches what it | |
| 314 | + // fetched, and the release page already carries binaries with that number. | |
| 315 | + skipInsideARelease(t) | |
| 316 | + if _, err := exec.LookPath("git"); err != nil { | |
| 317 | + t.Skip("git is not available") | |
| 318 | + } | |
| 319 | + | |
| 320 | + _, clone := throwawayClone(t) | |
| 321 | + | |
| 322 | + if out, err := runAllowingFailure(t, clone, "./01-release.tag.sh"); err != nil { | |
| 323 | + t.Fatalf("the first release failed:\n%s", out) | |
| 324 | + } | |
| 325 | + | |
| 326 | + out, err := runAllowingFailure(t, clone, "./01-release.tag.sh") | |
| 327 | + | |
| 328 | + if err == nil { | |
| 329 | + t.Fatalf("the script published the same tag twice:\n%s", out) | |
| 330 | + } | |
| 331 | + if !strings.Contains(out, "already exists") { | |
| 332 | + t.Errorf("the refusal does not say the tag is taken:\n%s", out) | |
| 333 | + } | |
| 334 | +} | |
| 335 | + | |
| 336 | +// throwawayClone sets up a bare remote and a clone of it holding a copy of | |
| 337 | +// this module and a release.env naming a test version, and returns both paths. | |
| 338 | +func throwawayClone(t *testing.T) (remote, clone string) { | |
| 339 | + t.Helper() | |
| 340 | + | |
| 341 | + root := t.TempDir() | |
| 342 | + remote = filepath.Join(root, "remote.git") | |
| 343 | + clone = filepath.Join(root, "clone") | |
| 344 | + | |
| 345 | + runOrFail(t, root, "git", "init", "--bare", "--initial-branch=main", remote) | |
| 346 | + runOrFail(t, root, "git", "clone", remote, clone) | |
| 347 | + copyModuleInto(t, clone) | |
| 348 | + runOrFail(t, clone, "git", "config", "user.email", "test@example.test") | |
| 349 | + runOrFail(t, clone, "git", "config", "user.name", "Release Test") | |
| 350 | + writeTestFile(t, filepath.Join(clone, "release.env"), "TAG=v0.0.1-test\nABOUT=\"a throwaway release\"\n") | |
| 351 | + return remote, clone | |
| 352 | +} | |
| 353 | + | |
| 354 | +// copyModuleInto copies the module's source into a directory, so the script can | |
| 355 | +// be run against a real checkout without touching this one. | |
| 356 | +// | |
| 357 | +// .git is left out because the target has its own; *.env because a test writes | |
| 358 | +// its own release.env — copying this checkout's would release whatever version | |
| 359 | +// happens to be in it; go.work because it would point the copy at a turbo-core | |
| 360 | +// checkout that is not what a release builds against; and the build outputs | |
| 361 | +// (bin, release, kits) and the demo project because they are hundreds of | |
| 362 | +// megabytes the script never reads. | |
| 363 | +// | |
| 364 | +// The copying is done here rather than by shelling out to cp, which on a | |
| 365 | +// network-backed working copy has been seen to write the right number of bytes | |
| 366 | +// and the wrong ones: every file in the copy came out NUL-filled. | |
| 367 | +func copyModuleInto(t *testing.T, target string) { | |
| 368 | + t.Helper() | |
| 369 | + | |
| 370 | + entries, err := os.ReadDir(".") | |
| 371 | + if err != nil { | |
| 372 | + t.Fatalf("reading the module: %v", err) | |
| 373 | + } | |
| 374 | + for _, entry := range entries { | |
| 375 | + if leftOutOfTheCopy(entry.Name()) { | |
| 376 | + continue | |
| 377 | + } | |
| 378 | + copyTree(t, entry.Name(), filepath.Join(target, entry.Name())) | |
| 379 | + } | |
| 380 | +} | |
| 381 | + | |
| 382 | +// leftOutOfTheCopy reports whether a top-level entry stays out of a throwaway | |
| 383 | +// copy of the module. | |
| 384 | +func leftOutOfTheCopy(name string) bool { | |
| 385 | + switch name { | |
| 386 | + case ".git", "bin", "release", "kits", "demo", "demos", "go.work", "go.work.sum": | |
| 387 | + return true | |
| 388 | + } | |
| 389 | + return strings.HasSuffix(name, ".env") | |
| 390 | +} | |
| 391 | + | |
| 392 | +// copyTree copies a file or a directory to a new path. | |
| 393 | +// | |
| 394 | +// Anything that is neither a regular file nor a directory is skipped: the tool | |
| 395 | +// directories beside the source hold symlinks into caches that do not exist in | |
| 396 | +// a temporary copy, and the release scripts have no use for them. | |
| 397 | +func copyTree(t *testing.T, from, to string) { | |
| 398 | + t.Helper() | |
| 399 | + | |
| 400 | + err := filepath.WalkDir(from, func(path string, entry os.DirEntry, err error) error { | |
| 401 | + if err != nil { | |
| 402 | + return err | |
| 403 | + } | |
| 404 | + relative, err := filepath.Rel(from, path) | |
| 405 | + if err != nil { | |
| 406 | + return err | |
| 407 | + } | |
| 408 | + destination := filepath.Join(to, relative) | |
| 409 | + | |
| 410 | + if entry.IsDir() { | |
| 411 | + return os.MkdirAll(destination, 0o755) | |
| 412 | + } | |
| 413 | + if !entry.Type().IsRegular() { | |
| 414 | + return nil | |
| 415 | + } | |
| 416 | + info, err := entry.Info() | |
| 417 | + if err != nil { | |
| 418 | + return err | |
| 419 | + } | |
| 420 | + data, err := os.ReadFile(path) | |
| 421 | + if err != nil { | |
| 422 | + return err | |
| 423 | + } | |
| 424 | + if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil { | |
| 425 | + return err | |
| 426 | + } | |
| 427 | + // The mode carries the execute bit, without which the scripts these | |
| 428 | + // tests exist to run cannot be run. | |
| 429 | + return os.WriteFile(destination, data, info.Mode().Perm()) | |
| 430 | + }) | |
| 431 | + if err != nil { | |
| 432 | + t.Fatalf("copying %s: %v", from, err) | |
| 433 | + } | |
| 434 | +} | |
| 435 | + | |
| 436 | +// runOrFail executes a command in a directory, failing the test if it does not | |
| 437 | +// succeed. | |
| 438 | +func runOrFail(t *testing.T, dir string, name string, args ...string) { | |
| 439 | + t.Helper() | |
| 440 | + | |
| 441 | + if out, err := runAllowingFailure(t, dir, name, args...); err != nil { | |
| 442 | + t.Fatalf("%s %v: %v\n%s", name, args, err, out) | |
| 443 | + } | |
| 444 | +} | |
| 445 | + | |
| 446 | +// runAllowingFailure executes a command and returns its combined output along | |
| 447 | +// with whether it succeeded. | |
| 448 | +// | |
| 449 | +// GOWORK is switched off for the child: a go.work beside this checkout points | |
| 450 | +// at a turbo-core working tree, and a release is built against the published | |
| 451 | +// module, which is what a clean clone would see. | |
| 452 | +func runAllowingFailure(t *testing.T, dir string, name string, args ...string) (string, error) { | |
| 453 | + t.Helper() | |
| 454 | + | |
| 455 | + command := exec.Command(name, args...) | |
| 456 | + command.Dir = dir | |
| 457 | + command.Env = append(os.Environ(), "GOWORK=off") | |
| 458 | + out, err := command.CombinedOutput() | |
| 459 | + return string(out), err | |
| 460 | +} | |
| 461 | + | |
| 462 | +// writeTestFile creates a file, failing the test if it cannot. | |
| 463 | +func writeTestFile(t *testing.T, path, contents string) { | |
| 464 | + t.Helper() | |
| 465 | + | |
| 466 | + if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { | |
| 467 | + t.Fatalf("writing %s: %v", path, err) | |
| 468 | + } | |
| 469 | +} | |
| 470 | + | |
| 471 | +func TestTheBuildScriptTakesTheTagFromTheCommandLine(t *testing.T) { | |
| 472 | + // release.env is git-ignored, so the workflow has none: it passes the tag | |
| 473 | + // it was started by. A script that only reads the file builds nothing in | |
| 474 | + // CI, or builds whatever version the file last named. | |
| 475 | + script := readReleaseScript(t) | |
| 476 | + | |
| 477 | + if !strings.Contains(script, `TAG="${1:-${TAG:-}}"`) { | |
| 478 | + t.Error("the build script does not take the tag from its first argument") | |
| 479 | + } | |
| 480 | + if !strings.Contains(script, `[ -f release.env ]`) { | |
| 481 | + t.Error("the build script requires release.env, which CI does not have") | |
| 482 | + } | |
| 483 | +} | |
| 484 | + | |
| 485 | +func TestTheBuildScriptRefusesATagThatIsNotAVersion(t *testing.T) { | |
| 486 | + // The proxy will not serve a tag it cannot read as a version, so a typo | |
| 487 | + // here builds perfectly and then fails at every `go install`. | |
| 488 | + // | |
| 489 | + // The refusal comes before anything is built or written, so the script | |
| 490 | + // alone is enough: it is run from an empty directory with no release.env. | |
| 491 | + dir := t.TempDir() | |
| 492 | + script, err := os.ReadFile("02-build-releases.sh") | |
| 493 | + if err != nil { | |
| 494 | + t.Fatalf("reading the build script: %v", err) | |
| 495 | + } | |
| 496 | + writeTestFile(t, filepath.Join(dir, "02-build-releases.sh"), string(script)) | |
| 497 | + | |
| 498 | + out, err := runAllowingFailure(t, dir, "bash", "./02-build-releases.sh", "v0.o.0") | |
| 499 | + | |
| 500 | + if err == nil { | |
| 501 | + t.Fatalf("the script accepted a tag that is not a version:\n%s", out) | |
| 502 | + } | |
| 503 | + if !strings.Contains(out, "v1.2.3") { | |
| 504 | + t.Errorf("the refusal does not say what a tag should look like:\n%s", out) | |
| 505 | + } | |
| 506 | +} | |
| 507 | + | |
| 508 | +func TestTheBuildScriptDoesNotHandOffToAnUploadScript(t *testing.T) { | |
| 509 | + // 04 attached the binaries to a release page a personal token had created. | |
| 510 | + // The workflow does both now; a script still pointing at 04 sends the | |
| 511 | + // reader to run something that is not there. | |
| 512 | + if strings.Contains(readReleaseScript(t), "04-release") { | |
| 513 | + t.Error("the build script still hands off to 04-release.upload-binaries.sh") | |
| 514 | + } | |
| 515 | +} | |
| 516 | + | |
| 517 | +// readWorkflow returns the release workflow's text. | |
| 518 | +func readWorkflow(t *testing.T) string { | |
| 519 | + t.Helper() | |
| 520 | + | |
| 521 | + data, err := os.ReadFile(filepath.Join(".github", "workflows", "release.yml")) | |
| 522 | + if err != nil { | |
| 523 | + t.Fatalf("reading the release workflow: %v", err) | |
| 524 | + } | |
| 525 | + return string(data) | |
| 526 | +} | |
| 527 | + | |
| 528 | +func TestTheWorkflowPublishesOnATagPush(t *testing.T) { | |
| 529 | + // The tag push is the trigger: ./01-release.tag.sh ends by pushing one, | |
| 530 | + // and nothing else starts a release. | |
| 531 | + workflow := readWorkflow(t) | |
| 532 | + | |
| 533 | + for _, want := range []string{"push:", "tags:", `- "v*"`} { | |
| 534 | + if !strings.Contains(workflow, want) { | |
| 535 | + t.Errorf("the workflow never declares %q", want) | |
| 536 | + } | |
| 537 | + } | |
| 538 | + // A workflow with the default read-only token cannot create a release, and | |
| 539 | + // fails at its last step after doing all the work. | |
| 540 | + if !strings.Contains(workflow, "contents: write") { | |
| 541 | + t.Error("the workflow does not ask for contents: write") | |
| 542 | + } | |
| 543 | +} | |
| 544 | + | |
| 545 | +func TestTheWorkflowBuildsWithTheSameScriptAPersonRuns(t *testing.T) { | |
| 546 | + // A CI job that builds its own way is a second pipeline nobody tests, and | |
| 547 | + // the local one is then only ever exercised by accident. | |
| 548 | + if !strings.Contains(readWorkflow(t), "./02-build-releases.sh") { | |
| 549 | + t.Error("the workflow does not build the release with ./02-build-releases.sh") | |
| 550 | + } | |
| 551 | +} | |
| 552 | + | |
| 553 | +func TestTheWorkflowAttachesWhatWasBuilt(t *testing.T) { | |
| 554 | + // Publishing a release page with no files attached is a silent half-job: | |
| 555 | + // the page exists and the downloads are not there. | |
| 556 | + workflow := readWorkflow(t) | |
| 557 | + | |
| 558 | + for _, want := range []string{"turbo-golo-*", "SHA256SUMS", "fail_on_unmatched_files: true"} { | |
| 559 | + if !strings.Contains(workflow, want) { | |
| 560 | + t.Errorf("the workflow never mentions %q", want) | |
| 561 | + } | |
| 562 | + } | |
| 563 | +} | |
| 564 | + | |
| 565 | +func TestTheWorkflowLinksToTheDocumentationAtThatTag(t *testing.T) { | |
| 566 | + // A release page is not inside the repository tree, so a relative path | |
| 567 | + // from it 404s — and a link to the branch would rot as the branch moves. | |
| 568 | + workflow := readWorkflow(t) | |
| 569 | + | |
| 570 | + if !strings.Contains(workflow, "blob/${GITHUB_REF_NAME}") { | |
| 571 | + t.Error("the release notes do not link into the repository at the released tag") | |
| 572 | + } | |
| 573 | + if !strings.Contains(workflow, "/docs/en/README.md") { | |
| 574 | + t.Error("the release notes do not link to the documentation") | |
| 575 | + } | |
| 576 | +} | |
| 577 | + | |
| 578 | +func TestTheWorkflowNeedsNoPersonalToken(t *testing.T) { | |
| 579 | + // The release API behind Rickub's /gh shim accepts the job's own | |
| 580 | + // GITHUB_TOKEN and refuses a personal one, so a secret referenced here is | |
| 581 | + // a credential that cannot work and still has to be kept somewhere. | |
| 582 | + if strings.Contains(readWorkflow(t), "secrets.") { | |
| 583 | + t.Error("the workflow reads a secret; the job's own token is the only credential the release API takes") | |
| 584 | + } | |
| 585 | +} | |
| 586 | + | |
| 587 | +func TestTheWorkflowDoesNotStartAReleaseInsideItself(t *testing.T) { | |
| 588 | + // The suite it runs includes tests that run ./01-release.tag.sh against a | |
| 589 | + // throwaway clone. Locally the script exports this before calling make; | |
| 590 | + // in CI nothing calls the script, so the job has to set it itself. | |
| 591 | + if !strings.Contains(readWorkflow(t), "TURBO_GOLO_RELEASING") { | |
| 592 | + t.Error("the workflow runs the suite without TURBO_GOLO_RELEASING set") | |
| 593 | + } | |
| 594 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,594 @@ | |||
| 1 | +package main | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "os" | ||
| 5 | + "os/exec" | ||
| 6 | + "path/filepath" | ||
| 7 | + "runtime" | ||
| 8 | + "strings" | ||
| 9 | + "testing" | ||
| 10 | +) | ||
| 11 | + | ||
| 12 | +// readReleaseScript returns the release builder, so its rules can be asserted | ||
| 13 | +// without running it: running it cross-compiles five binaries, which is not a | ||
| 14 | +// unit test. (Running the tagging script, on the other hand, is done below, | ||
| 15 | +// against a throwaway clone.) | ||
| 16 | +func readReleaseScript(t *testing.T) string { | ||
| 17 | + t.Helper() | ||
| 18 | + | ||
| 19 | + script, err := os.ReadFile("02-build-releases.sh") | ||
| 20 | + if err != nil { | ||
| 21 | + t.Fatalf("cannot read the release script: %v", err) | ||
| 22 | + } | ||
| 23 | + return string(script) | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +func TestTheReleaseScriptStampsTheBinariesItShips(t *testing.T) { | ||
| 27 | + // Without -ldflags on the cross-compile, every downloaded binary reports | ||
| 28 | + // "devel" while the release page names a version. The host binary would | ||
| 29 | + // still be right, so nothing but this notices. | ||
| 30 | + script := readReleaseScript(t) | ||
| 31 | + | ||
| 32 | + build := commandContaining(t, script, "GOARCH=") | ||
| 33 | + if !strings.Contains(build, "-ldflags") { | ||
| 34 | + t.Errorf("the cross-compile does not stamp a version:\n%s", build) | ||
| 35 | + } | ||
| 36 | +} | ||
| 37 | + | ||
| 38 | +func TestTheReleaseScriptTakesTheStampFromTheMakefile(t *testing.T) { | ||
| 39 | + // Repeating the -X paths in the script is how the host binary and the | ||
| 40 | + // downloads would come to disagree about which package holds the version. | ||
| 41 | + script := readReleaseScript(t) | ||
| 42 | + | ||
| 43 | + if !strings.Contains(script, "make --no-print-directory ldflags") { | ||
| 44 | + t.Error("the script does not read the linker flags from the Makefile") | ||
| 45 | + } | ||
| 46 | + if strings.Contains(script, "version.stamp=") { | ||
| 47 | + t.Error("the script spells out the -X path, which the Makefile already owns") | ||
| 48 | + } | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +func TestTheReleaseScriptStampsTheTagItIsReleasing(t *testing.T) { | ||
| 52 | + // The release *is* ${TAG}, so that is what the binaries say. Letting the | ||
| 53 | + // Makefile's default stand would stamp `git describe`, which answers a | ||
| 54 | + // different question — where HEAD is — and disagrees the moment anyone | ||
| 55 | + // commits after tagging. | ||
| 56 | + script := readReleaseScript(t) | ||
| 57 | + | ||
| 58 | + flags := commandContaining(t, script, "ldflags") | ||
| 59 | + if !strings.Contains(flags, `VERSION="${TAG}"`) { | ||
| 60 | + t.Errorf("the stamp does not come from TAG:\n%s", flags) | ||
| 61 | + } | ||
| 62 | + if build := commandContaining(t, script, "make build"); !strings.Contains(build, `VERSION="${TAG}"`) { | ||
| 63 | + t.Errorf("the host build carries a different version from the assets:\n%s", build) | ||
| 64 | + } | ||
| 65 | +} | ||
| 66 | + | ||
| 67 | +func TestTheReleaseScriptDoesNotParseTheVersionOutOfProse(t *testing.T) { | ||
| 68 | + // `-version` is written for a person and has changed shape once already; | ||
| 69 | + // awk '{print $NF}' on it read a timestamp and failed a release. | ||
| 70 | + script := readReleaseScript(t) | ||
| 71 | + | ||
| 72 | + if strings.Contains(script, "$NF") { | ||
| 73 | + t.Error("the script reads a field out of the -version line, which is prose") | ||
| 74 | + } | ||
| 75 | +} | ||
| 76 | + | ||
| 77 | +func TestTheMakefileHandsOutTheFlagsThatStampABuild(t *testing.T) { | ||
| 78 | + // The contract the release script depends on: `make ldflags` prints flags | ||
| 79 | + // that actually put *the Makefile's own version* into a binary. | ||
| 80 | + // | ||
| 81 | + // It is checked against `make version` rather than against "not devel", | ||
| 82 | + // because a checkout with no tags — a fresh clone, or a repository that has | ||
| 83 | + // never had a release — correctly reports devel, and a test that called | ||
| 84 | + // that a failure would be testing the tags rather than the flags. | ||
| 85 | + version, err := exec.Command("make", "--no-print-directory", "version").Output() | ||
| 86 | + if err != nil { | ||
| 87 | + t.Fatalf("make version: %v", err) | ||
| 88 | + } | ||
| 89 | + // internal/version drops the leading v of a tag, so the comparison has to | ||
| 90 | + // as well: `make version` says v0.2.1 and the binary says 0.2.1. | ||
| 91 | + number := strings.TrimPrefix(strings.Fields(strings.TrimSpace(string(version)))[0], "v") | ||
| 92 | + | ||
| 93 | + flags, err := exec.Command("make", "--no-print-directory", "ldflags").Output() | ||
| 94 | + if err != nil { | ||
| 95 | + t.Fatalf("make ldflags: %v", err) | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | ||
| 99 | + build := exec.Command("go", "build", "-trimpath", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".") | ||
| 100 | + build.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH) | ||
| 101 | + if out, err := build.CombinedOutput(); err != nil { | ||
| 102 | + t.Fatalf("building with those flags failed: %v\n%s", err, out) | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + reported, err := exec.Command(binary, "-version").Output() | ||
| 106 | + if err != nil { | ||
| 107 | + t.Fatalf("the stamped binary does not run: %v", err) | ||
| 108 | + } | ||
| 109 | + if !strings.Contains(string(reported), number) { | ||
| 110 | + t.Errorf("-version printed %q, want it to carry the Makefile's version %q", reported, number) | ||
| 111 | + } | ||
| 112 | + if strings.Contains(string(reported), "unknown") { | ||
| 113 | + t.Errorf("-version printed %q, so nothing reached the linker at all", reported) | ||
| 114 | + } | ||
| 115 | +} | ||
| 116 | + | ||
| 117 | +func TestMakeLdflagsTakesTheVersionItIsGiven(t *testing.T) { | ||
| 118 | + // The release script overrides VERSION with the tag it is releasing, and | ||
| 119 | + // everything downstream rests on that override reaching the linker. | ||
| 120 | + flags, err := exec.Command("make", "--no-print-directory", "ldflags", "VERSION=v9.9.9").Output() | ||
| 121 | + if err != nil { | ||
| 122 | + t.Fatalf("make ldflags: %v", err) | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | ||
| 126 | + build := exec.Command("go", "build", "-trimpath", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".") | ||
| 127 | + build.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH) | ||
| 128 | + if out, err := build.CombinedOutput(); err != nil { | ||
| 129 | + t.Fatalf("building with those flags failed: %v\n%s", err, out) | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + reported, err := exec.Command(binary, "-version").Output() | ||
| 133 | + if err != nil { | ||
| 134 | + t.Fatalf("the stamped binary does not run: %v", err) | ||
| 135 | + } | ||
| 136 | + if !strings.Contains(string(reported), "9.9.9") { | ||
| 137 | + t.Errorf("-version printed %q, so VERSION=v9.9.9 never reached the linker", reported) | ||
| 138 | + } | ||
| 139 | +} | ||
| 140 | + | ||
| 141 | +// commandContaining returns the first shell command of a script holding a | ||
| 142 | +// fragment, with backslash continuations joined: a command's flags are often | ||
| 143 | +// on the line after the one that names it, and a test about the command should | ||
| 144 | +// not depend on where it happens to wrap. | ||
| 145 | +func commandContaining(t *testing.T, script, fragment string) string { | ||
| 146 | + t.Helper() | ||
| 147 | + | ||
| 148 | + joined := strings.ReplaceAll(script, "\\\n", " ") | ||
| 149 | + for _, line := range strings.Split(joined, "\n") { | ||
| 150 | + if strings.Contains(line, fragment) { | ||
| 151 | + return strings.TrimSpace(line) | ||
| 152 | + } | ||
| 153 | + } | ||
| 154 | + t.Fatalf("no command in the script contains %q", fragment) | ||
| 155 | + return "" | ||
| 156 | +} | ||
| 157 | + | ||
| 158 | +// readTagScript returns the tagging script, whose failure modes are what the | ||
| 159 | +// release builder is left to notice when they are not caught here. | ||
| 160 | +func readTagScript(t *testing.T) string { | ||
| 161 | + t.Helper() | ||
| 162 | + | ||
| 163 | + script, err := os.ReadFile("01-release.tag.sh") | ||
| 164 | + if err != nil { | ||
| 165 | + t.Fatalf("cannot read the tagging script: %v", err) | ||
| 166 | + } | ||
| 167 | + return string(script) | ||
| 168 | +} | ||
| 169 | + | ||
| 170 | +func TestTheTagScriptStopsOnTheFirstFailure(t *testing.T) { | ||
| 171 | + // Without this, `git tag` refusing a tag that already existed was skipped | ||
| 172 | + // in silence and the `git push` after it pushed the OLD tag, cutting a | ||
| 173 | + // release from a commit nobody meant. | ||
| 174 | + if !strings.Contains(readTagScript(t), "set -euo pipefail") { | ||
| 175 | + t.Error("the tagging script does not stop on a failing step") | ||
| 176 | + } | ||
| 177 | +} | ||
| 178 | + | ||
| 179 | +func TestTheTagScriptRefusesATagThatAlreadyExists(t *testing.T) { | ||
| 180 | + script := readTagScript(t) | ||
| 181 | + | ||
| 182 | + for _, want := range []string{ | ||
| 183 | + "git rev-parse -q --verify", // taken locally | ||
| 184 | + "git ls-remote --tags origin", // taken on the remote, after a local delete | ||
| 185 | + } { | ||
| 186 | + if !strings.Contains(script, want) { | ||
| 187 | + t.Errorf("the tagging script never checks %q", want) | ||
| 188 | + } | ||
| 189 | + } | ||
| 190 | +} | ||
| 191 | + | ||
| 192 | +func TestTheTagScriptSurvivesHavingNothingToCommit(t *testing.T) { | ||
| 193 | + // Under `set -e` a plain `git commit` with a clean tree ends the release, | ||
| 194 | + // which is wrong: the work being already committed is the normal case on a | ||
| 195 | + // second run. | ||
| 196 | + script := readTagScript(t) | ||
| 197 | + | ||
| 198 | + if !strings.Contains(script, "git diff --cached --quiet") { | ||
| 199 | + t.Error("the tagging script commits without checking there is anything to commit") | ||
| 200 | + } | ||
| 201 | +} | ||
| 202 | + | ||
| 203 | +func TestTheTagScriptTagsOnlyAfterThePushSucceeded(t *testing.T) { | ||
| 204 | + // A tag left behind pointing at a commit the remote has never seen is the | ||
| 205 | + // state that needs a force push to escape. | ||
| 206 | + script := readTagScript(t) | ||
| 207 | + | ||
| 208 | + push := strings.Index(script, `git push origin "$(git rev-parse`) | ||
| 209 | + tag := strings.Index(script, `git tag -a "${TAG}"`) | ||
| 210 | + if push < 0 || tag < 0 { | ||
| 211 | + t.Fatal("the tagging script no longer pushes and tags") | ||
| 212 | + } | ||
| 213 | + if tag < push { | ||
| 214 | + t.Error("the script tags before pushing, so a rejected push leaves a stray tag") | ||
| 215 | + } | ||
| 216 | +} | ||
| 217 | + | ||
| 218 | +// skipInsideARelease stops a test that runs the tagging script from running | ||
| 219 | +// while the tagging script is running it. | ||
| 220 | +// | ||
| 221 | +// The script sets this before `make check`, and `make check` runs this suite. | ||
| 222 | +// Without the guard the two call each other forever — which is not a test-only | ||
| 223 | +// hazard: a real release would recurse in exactly the same way. The Release | ||
| 224 | +// workflow sets it too, for the same reason. | ||
| 225 | +func skipInsideARelease(t *testing.T) { | ||
| 226 | + t.Helper() | ||
| 227 | + if os.Getenv("TURBO_GOLO_RELEASING") != "" { | ||
| 228 | + t.Skip("running inside a release; not starting another one") | ||
| 229 | + } | ||
| 230 | +} | ||
| 231 | + | ||
| 232 | +func TestTheTagScriptRunsTheSuiteBeforePublishing(t *testing.T) { | ||
| 233 | + // A version people will download, and the proxy will cache, is the wrong | ||
| 234 | + // place to find out the suite was red. | ||
| 235 | + if !strings.Contains(readTagScript(t), "make --no-print-directory check") { | ||
| 236 | + t.Error("the tagging script publishes without running make check") | ||
| 237 | + } | ||
| 238 | +} | ||
| 239 | + | ||
| 240 | +func TestTheTagScriptRefusesAReplaceDirective(t *testing.T) { | ||
| 241 | + // The proxy serves go.mod as written, so `go install …@TAG` on a module | ||
| 242 | + // carrying a replace looks for turbo-core in a directory that does not | ||
| 243 | + // exist on the installer's machine. | ||
| 244 | + if !strings.Contains(readTagScript(t), "replace") { | ||
| 245 | + t.Error("the tagging script does not check go.mod for a replace directive") | ||
| 246 | + } | ||
| 247 | +} | ||
| 248 | + | ||
| 249 | +func TestThisModuleHasNoReplaceDirective(t *testing.T) { | ||
| 250 | + // The check above only helps if it is true today as well. | ||
| 251 | + data, err := os.ReadFile("go.mod") | ||
| 252 | + if err != nil { | ||
| 253 | + t.Fatalf("reading go.mod: %v", err) | ||
| 254 | + } | ||
| 255 | + for _, line := range strings.Split(string(data), "\n") { | ||
| 256 | + if strings.HasPrefix(strings.TrimSpace(line), "replace ") { | ||
| 257 | + t.Errorf("go.mod carries %q; a published module must not", line) | ||
| 258 | + } | ||
| 259 | + } | ||
| 260 | +} | ||
| 261 | + | ||
| 262 | +func TestTheReleaseToolingNeedsNoPersonalToken(t *testing.T) { | ||
| 263 | + // The Release workflow publishes with the job's own GITHUB_TOKEN, which is | ||
| 264 | + // the only credential Rickub's release API accepts. A script still reading | ||
| 265 | + // a token file is a credential that cannot work and has to be kept | ||
| 266 | + // somewhere all the same — and a 02 or 04 left in the tree is a second | ||
| 267 | + // pipeline somebody will run by mistake. | ||
| 268 | + for _, script := range []string{"01-release.tag.sh", "02-build-releases.sh"} { | ||
| 269 | + data, err := os.ReadFile(script) | ||
| 270 | + if err != nil { | ||
| 271 | + t.Fatalf("reading %s: %v", script, err) | ||
| 272 | + } | ||
| 273 | + for _, secret := range []string{"token.env", "${TOKEN}"} { | ||
| 274 | + if strings.Contains(string(data), secret) { | ||
| 275 | + t.Errorf("%s still reads %s", script, secret) | ||
| 276 | + } | ||
| 277 | + } | ||
| 278 | + } | ||
| 279 | + for _, gone := range []string{"02-release.publish.sh", "04-release.upload-binaries.sh"} { | ||
| 280 | + if _, err := os.Stat(gone); err == nil { | ||
| 281 | + t.Errorf("%s is still there; the workflow publishes and attaches the binaries now", gone) | ||
| 282 | + } | ||
| 283 | + } | ||
| 284 | +} | ||
| 285 | + | ||
| 286 | +func TestTheTagScriptTagsAndPushesForReal(t *testing.T) { | ||
| 287 | + // The whole flow, in a throwaway clone with its own bare remote, so no tag | ||
| 288 | + // is ever created in the real repository. Reading the script is not the | ||
| 289 | + // same as running it: every guard above was added because one of them was | ||
| 290 | + // wrong once. | ||
| 291 | + skipInsideARelease(t) | ||
| 292 | + if _, err := exec.LookPath("git"); err != nil { | ||
| 293 | + t.Skip("git is not available") | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + remote, clone := throwawayClone(t) | ||
| 297 | + | ||
| 298 | + out, err := runAllowingFailure(t, clone, "./01-release.tag.sh") | ||
| 299 | + if err != nil { | ||
| 300 | + t.Fatalf("the tagging script failed:\n%s", out) | ||
| 301 | + } | ||
| 302 | + if !strings.Contains(out, "published") { | ||
| 303 | + t.Errorf("the script did not report publishing:\n%s", out) | ||
| 304 | + } | ||
| 305 | + | ||
| 306 | + tags, _ := runAllowingFailure(t, remote, "git", "tag") | ||
| 307 | + if !strings.Contains(tags, "v0.0.1-test") { | ||
| 308 | + t.Errorf("the remote has tags %q, want v0.0.1-test", strings.TrimSpace(tags)) | ||
| 309 | + } | ||
| 310 | +} | ||
| 311 | + | ||
| 312 | +func TestTheTagScriptRefusesATagItAlreadyPublished(t *testing.T) { | ||
| 313 | + // Moving a published version is not an option: the proxy caches what it | ||
| 314 | + // fetched, and the release page already carries binaries with that number. | ||
| 315 | + skipInsideARelease(t) | ||
| 316 | + if _, err := exec.LookPath("git"); err != nil { | ||
| 317 | + t.Skip("git is not available") | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + _, clone := throwawayClone(t) | ||
| 321 | + | ||
| 322 | + if out, err := runAllowingFailure(t, clone, "./01-release.tag.sh"); err != nil { | ||
| 323 | + t.Fatalf("the first release failed:\n%s", out) | ||
| 324 | + } | ||
| 325 | + | ||
| 326 | + out, err := runAllowingFailure(t, clone, "./01-release.tag.sh") | ||
| 327 | + | ||
| 328 | + if err == nil { | ||
| 329 | + t.Fatalf("the script published the same tag twice:\n%s", out) | ||
| 330 | + } | ||
| 331 | + if !strings.Contains(out, "already exists") { | ||
| 332 | + t.Errorf("the refusal does not say the tag is taken:\n%s", out) | ||
| 333 | + } | ||
| 334 | +} | ||
| 335 | + | ||
| 336 | +// throwawayClone sets up a bare remote and a clone of it holding a copy of | ||
| 337 | +// this module and a release.env naming a test version, and returns both paths. | ||
| 338 | +func throwawayClone(t *testing.T) (remote, clone string) { | ||
| 339 | + t.Helper() | ||
| 340 | + | ||
| 341 | + root := t.TempDir() | ||
| 342 | + remote = filepath.Join(root, "remote.git") | ||
| 343 | + clone = filepath.Join(root, "clone") | ||
| 344 | + | ||
| 345 | + runOrFail(t, root, "git", "init", "--bare", "--initial-branch=main", remote) | ||
| 346 | + runOrFail(t, root, "git", "clone", remote, clone) | ||
| 347 | + copyModuleInto(t, clone) | ||
| 348 | + runOrFail(t, clone, "git", "config", "user.email", "test@example.test") | ||
| 349 | + runOrFail(t, clone, "git", "config", "user.name", "Release Test") | ||
| 350 | + writeTestFile(t, filepath.Join(clone, "release.env"), "TAG=v0.0.1-test\nABOUT=\"a throwaway release\"\n") | ||
| 351 | + return remote, clone | ||
| 352 | +} | ||
| 353 | + | ||
| 354 | +// copyModuleInto copies the module's source into a directory, so the script can | ||
| 355 | +// be run against a real checkout without touching this one. | ||
| 356 | +// | ||
| 357 | +// .git is left out because the target has its own; *.env because a test writes | ||
| 358 | +// its own release.env — copying this checkout's would release whatever version | ||
| 359 | +// happens to be in it; go.work because it would point the copy at a turbo-core | ||
| 360 | +// checkout that is not what a release builds against; and the build outputs | ||
| 361 | +// (bin, release, kits) and the demo project because they are hundreds of | ||
| 362 | +// megabytes the script never reads. | ||
| 363 | +// | ||
| 364 | +// The copying is done here rather than by shelling out to cp, which on a | ||
| 365 | +// network-backed working copy has been seen to write the right number of bytes | ||
| 366 | +// and the wrong ones: every file in the copy came out NUL-filled. | ||
| 367 | +func copyModuleInto(t *testing.T, target string) { | ||
| 368 | + t.Helper() | ||
| 369 | + | ||
| 370 | + entries, err := os.ReadDir(".") | ||
| 371 | + if err != nil { | ||
| 372 | + t.Fatalf("reading the module: %v", err) | ||
| 373 | + } | ||
| 374 | + for _, entry := range entries { | ||
| 375 | + if leftOutOfTheCopy(entry.Name()) { | ||
| 376 | + continue | ||
| 377 | + } | ||
| 378 | + copyTree(t, entry.Name(), filepath.Join(target, entry.Name())) | ||
| 379 | + } | ||
| 380 | +} | ||
| 381 | + | ||
| 382 | +// leftOutOfTheCopy reports whether a top-level entry stays out of a throwaway | ||
| 383 | +// copy of the module. | ||
| 384 | +func leftOutOfTheCopy(name string) bool { | ||
| 385 | + switch name { | ||
| 386 | + case ".git", "bin", "release", "kits", "demo", "demos", "go.work", "go.work.sum": | ||
| 387 | + return true | ||
| 388 | + } | ||
| 389 | + return strings.HasSuffix(name, ".env") | ||
| 390 | +} | ||
| 391 | + | ||
| 392 | +// copyTree copies a file or a directory to a new path. | ||
| 393 | +// | ||
| 394 | +// Anything that is neither a regular file nor a directory is skipped: the tool | ||
| 395 | +// directories beside the source hold symlinks into caches that do not exist in | ||
| 396 | +// a temporary copy, and the release scripts have no use for them. | ||
| 397 | +func copyTree(t *testing.T, from, to string) { | ||
| 398 | + t.Helper() | ||
| 399 | + | ||
| 400 | + err := filepath.WalkDir(from, func(path string, entry os.DirEntry, err error) error { | ||
| 401 | + if err != nil { | ||
| 402 | + return err | ||
| 403 | + } | ||
| 404 | + relative, err := filepath.Rel(from, path) | ||
| 405 | + if err != nil { | ||
| 406 | + return err | ||
| 407 | + } | ||
| 408 | + destination := filepath.Join(to, relative) | ||
| 409 | + | ||
| 410 | + if entry.IsDir() { | ||
| 411 | + return os.MkdirAll(destination, 0o755) | ||
| 412 | + } | ||
| 413 | + if !entry.Type().IsRegular() { | ||
| 414 | + return nil | ||
| 415 | + } | ||
| 416 | + info, err := entry.Info() | ||
| 417 | + if err != nil { | ||
| 418 | + return err | ||
| 419 | + } | ||
| 420 | + data, err := os.ReadFile(path) | ||
| 421 | + if err != nil { | ||
| 422 | + return err | ||
| 423 | + } | ||
| 424 | + if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil { | ||
| 425 | + return err | ||
| 426 | + } | ||
| 427 | + // The mode carries the execute bit, without which the scripts these | ||
| 428 | + // tests exist to run cannot be run. | ||
| 429 | + return os.WriteFile(destination, data, info.Mode().Perm()) | ||
| 430 | + }) | ||
| 431 | + if err != nil { | ||
| 432 | + t.Fatalf("copying %s: %v", from, err) | ||
| 433 | + } | ||
| 434 | +} | ||
| 435 | + | ||
| 436 | +// runOrFail executes a command in a directory, failing the test if it does not | ||
| 437 | +// succeed. | ||
| 438 | +func runOrFail(t *testing.T, dir string, name string, args ...string) { | ||
| 439 | + t.Helper() | ||
| 440 | + | ||
| 441 | + if out, err := runAllowingFailure(t, dir, name, args...); err != nil { | ||
| 442 | + t.Fatalf("%s %v: %v\n%s", name, args, err, out) | ||
| 443 | + } | ||
| 444 | +} | ||
| 445 | + | ||
| 446 | +// runAllowingFailure executes a command and returns its combined output along | ||
| 447 | +// with whether it succeeded. | ||
| 448 | +// | ||
| 449 | +// GOWORK is switched off for the child: a go.work beside this checkout points | ||
| 450 | +// at a turbo-core working tree, and a release is built against the published | ||
| 451 | +// module, which is what a clean clone would see. | ||
| 452 | +func runAllowingFailure(t *testing.T, dir string, name string, args ...string) (string, error) { | ||
| 453 | + t.Helper() | ||
| 454 | + | ||
| 455 | + command := exec.Command(name, args...) | ||
| 456 | + command.Dir = dir | ||
| 457 | + command.Env = append(os.Environ(), "GOWORK=off") | ||
| 458 | + out, err := command.CombinedOutput() | ||
| 459 | + return string(out), err | ||
| 460 | +} | ||
| 461 | + | ||
| 462 | +// writeTestFile creates a file, failing the test if it cannot. | ||
| 463 | +func writeTestFile(t *testing.T, path, contents string) { | ||
| 464 | + t.Helper() | ||
| 465 | + | ||
| 466 | + if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { | ||
| 467 | + t.Fatalf("writing %s: %v", path, err) | ||
| 468 | + } | ||
| 469 | +} | ||
| 470 | + | ||
| 471 | +func TestTheBuildScriptTakesTheTagFromTheCommandLine(t *testing.T) { | ||
| 472 | + // release.env is git-ignored, so the workflow has none: it passes the tag | ||
| 473 | + // it was started by. A script that only reads the file builds nothing in | ||
| 474 | + // CI, or builds whatever version the file last named. | ||
| 475 | + script := readReleaseScript(t) | ||
| 476 | + | ||
| 477 | + if !strings.Contains(script, `TAG="${1:-${TAG:-}}"`) { | ||
| 478 | + t.Error("the build script does not take the tag from its first argument") | ||
| 479 | + } | ||
| 480 | + if !strings.Contains(script, `[ -f release.env ]`) { | ||
| 481 | + t.Error("the build script requires release.env, which CI does not have") | ||
| 482 | + } | ||
| 483 | +} | ||
| 484 | + | ||
| 485 | +func TestTheBuildScriptRefusesATagThatIsNotAVersion(t *testing.T) { | ||
| 486 | + // The proxy will not serve a tag it cannot read as a version, so a typo | ||
| 487 | + // here builds perfectly and then fails at every `go install`. | ||
| 488 | + // | ||
| 489 | + // The refusal comes before anything is built or written, so the script | ||
| 490 | + // alone is enough: it is run from an empty directory with no release.env. | ||
| 491 | + dir := t.TempDir() | ||
| 492 | + script, err := os.ReadFile("02-build-releases.sh") | ||
| 493 | + if err != nil { | ||
| 494 | + t.Fatalf("reading the build script: %v", err) | ||
| 495 | + } | ||
| 496 | + writeTestFile(t, filepath.Join(dir, "02-build-releases.sh"), string(script)) | ||
| 497 | + | ||
| 498 | + out, err := runAllowingFailure(t, dir, "bash", "./02-build-releases.sh", "v0.o.0") | ||
| 499 | + | ||
| 500 | + if err == nil { | ||
| 501 | + t.Fatalf("the script accepted a tag that is not a version:\n%s", out) | ||
| 502 | + } | ||
| 503 | + if !strings.Contains(out, "v1.2.3") { | ||
| 504 | + t.Errorf("the refusal does not say what a tag should look like:\n%s", out) | ||
| 505 | + } | ||
| 506 | +} | ||
| 507 | + | ||
| 508 | +func TestTheBuildScriptDoesNotHandOffToAnUploadScript(t *testing.T) { | ||
| 509 | + // 04 attached the binaries to a release page a personal token had created. | ||
| 510 | + // The workflow does both now; a script still pointing at 04 sends the | ||
| 511 | + // reader to run something that is not there. | ||
| 512 | + if strings.Contains(readReleaseScript(t), "04-release") { | ||
| 513 | + t.Error("the build script still hands off to 04-release.upload-binaries.sh") | ||
| 514 | + } | ||
| 515 | +} | ||
| 516 | + | ||
| 517 | +// readWorkflow returns the release workflow's text. | ||
| 518 | +func readWorkflow(t *testing.T) string { | ||
| 519 | + t.Helper() | ||
| 520 | + | ||
| 521 | + data, err := os.ReadFile(filepath.Join(".github", "workflows", "release.yml")) | ||
| 522 | + if err != nil { | ||
| 523 | + t.Fatalf("reading the release workflow: %v", err) | ||
| 524 | + } | ||
| 525 | + return string(data) | ||
| 526 | +} | ||
| 527 | + | ||
| 528 | +func TestTheWorkflowPublishesOnATagPush(t *testing.T) { | ||
| 529 | + // The tag push is the trigger: ./01-release.tag.sh ends by pushing one, | ||
| 530 | + // and nothing else starts a release. | ||
| 531 | + workflow := readWorkflow(t) | ||
| 532 | + | ||
| 533 | + for _, want := range []string{"push:", "tags:", `- "v*"`} { | ||
| 534 | + if !strings.Contains(workflow, want) { | ||
| 535 | + t.Errorf("the workflow never declares %q", want) | ||
| 536 | + } | ||
| 537 | + } | ||
| 538 | + // A workflow with the default read-only token cannot create a release, and | ||
| 539 | + // fails at its last step after doing all the work. | ||
| 540 | + if !strings.Contains(workflow, "contents: write") { | ||
| 541 | + t.Error("the workflow does not ask for contents: write") | ||
| 542 | + } | ||
| 543 | +} | ||
| 544 | + | ||
| 545 | +func TestTheWorkflowBuildsWithTheSameScriptAPersonRuns(t *testing.T) { | ||
| 546 | + // A CI job that builds its own way is a second pipeline nobody tests, and | ||
| 547 | + // the local one is then only ever exercised by accident. | ||
| 548 | + if !strings.Contains(readWorkflow(t), "./02-build-releases.sh") { | ||
| 549 | + t.Error("the workflow does not build the release with ./02-build-releases.sh") | ||
| 550 | + } | ||
| 551 | +} | ||
| 552 | + | ||
| 553 | +func TestTheWorkflowAttachesWhatWasBuilt(t *testing.T) { | ||
| 554 | + // Publishing a release page with no files attached is a silent half-job: | ||
| 555 | + // the page exists and the downloads are not there. | ||
| 556 | + workflow := readWorkflow(t) | ||
| 557 | + | ||
| 558 | + for _, want := range []string{"turbo-golo-*", "SHA256SUMS", "fail_on_unmatched_files: true"} { | ||
| 559 | + if !strings.Contains(workflow, want) { | ||
| 560 | + t.Errorf("the workflow never mentions %q", want) | ||
| 561 | + } | ||
| 562 | + } | ||
| 563 | +} | ||
| 564 | + | ||
| 565 | +func TestTheWorkflowLinksToTheDocumentationAtThatTag(t *testing.T) { | ||
| 566 | + // A release page is not inside the repository tree, so a relative path | ||
| 567 | + // from it 404s — and a link to the branch would rot as the branch moves. | ||
| 568 | + workflow := readWorkflow(t) | ||
| 569 | + | ||
| 570 | + if !strings.Contains(workflow, "blob/${GITHUB_REF_NAME}") { | ||
| 571 | + t.Error("the release notes do not link into the repository at the released tag") | ||
| 572 | + } | ||
| 573 | + if !strings.Contains(workflow, "/docs/en/README.md") { | ||
| 574 | + t.Error("the release notes do not link to the documentation") | ||
| 575 | + } | ||
| 576 | +} | ||
| 577 | + | ||
| 578 | +func TestTheWorkflowNeedsNoPersonalToken(t *testing.T) { | ||
| 579 | + // The release API behind Rickub's /gh shim accepts the job's own | ||
| 580 | + // GITHUB_TOKEN and refuses a personal one, so a secret referenced here is | ||
| 581 | + // a credential that cannot work and still has to be kept somewhere. | ||
| 582 | + if strings.Contains(readWorkflow(t), "secrets.") { | ||
| 583 | + t.Error("the workflow reads a secret; the job's own token is the only credential the release API takes") | ||
| 584 | + } | ||
| 585 | +} | ||
| 586 | + | ||
| 587 | +func TestTheWorkflowDoesNotStartAReleaseInsideItself(t *testing.T) { | ||
| 588 | + // The suite it runs includes tests that run ./01-release.tag.sh against a | ||
| 589 | + // throwaway clone. Locally the script exports this before calling make; | ||
| 590 | + // in CI nothing calls the script, so the job has to set it itself. | ||
| 591 | + if !strings.Contains(readWorkflow(t), "TURBO_GOLO_RELEASING") { | ||
| 592 | + t.Error("the workflow runs the suite without TURBO_GOLO_RELEASING set") | ||
| 593 | + } | ||
| 594 | +} | ||
added
scripts/check-version.sh +72 -0 | new file mode 100755 | ||
| @@ -0,0 +1,72 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# | |
| 3 | +# Check that a freshly built binary reports the version the build meant to put | |
| 4 | +# into it. | |
| 5 | +# | |
| 6 | +# scripts/check-version.sh bin/turbo-golo v0.2.0 88a4c38 | |
| 7 | +# scripts/check-version.sh bin/turbo-golo # unstamped build | |
| 8 | +# | |
| 9 | +# Linker flags are a string: a typo in one produces a binary that builds, links | |
| 10 | +# and runs, and quietly reports the wrong version — or "unknown". Nothing but | |
| 11 | +# running the binary catches that, so the build runs it. | |
| 12 | +# | |
| 13 | +# With a version to expect, the reported number must **equal** it. A substring | |
| 14 | +# test is not enough: "0.2.0" is a substring of "10.2.0" and of a commit hash | |
| 15 | +# that happens to contain it, and the case this exists to catch is a stamp that | |
| 16 | +# is nearly right. | |
| 17 | +# | |
| 18 | +# With no version to expect — a build outside a git checkout, where there is | |
| 19 | +# nothing to describe — the only claim left is that some source named it, so | |
| 20 | +# "unknown" is the failure. | |
| 21 | + | |
| 22 | +set -euo pipefail | |
| 23 | + | |
| 24 | +if [ $# -lt 1 ]; then | |
| 25 | + echo "usage: $0 <binary> [expected-version] [expected-commit]" >&2 | |
| 26 | + exit 2 | |
| 27 | +fi | |
| 28 | + | |
| 29 | +readonly BINARY="$1" | |
| 30 | +readonly EXPECTED_VERSION="${2:-}" | |
| 31 | +readonly EXPECTED_COMMIT="${3:-}" | |
| 32 | + | |
| 33 | +if [ ! -x "${BINARY}" ]; then | |
| 34 | + echo "check-version: ${BINARY} is not an executable file" >&2 | |
| 35 | + exit 1 | |
| 36 | +fi | |
| 37 | + | |
| 38 | +if ! reported="$("${BINARY}" -version 2>&1)"; then | |
| 39 | + echo "check-version: ${BINARY} does not run:" >&2 | |
| 40 | + echo "${reported}" >&2 | |
| 41 | + exit 1 | |
| 42 | +fi | |
| 43 | + | |
| 44 | +# The binary prints "<Name> <number>" or "<Name> <number> (<commit>, built …)", | |
| 45 | +# so the number is the last field before the parenthesis, if there is one. The | |
| 46 | +# name is two words in every editor built on turbo-core and one word in some | |
| 47 | +# future one, which is why it is read from the right rather than the left. | |
| 48 | +head="${reported%% (*}" | |
| 49 | +number="${head##* }" | |
| 50 | + | |
| 51 | +if [ -n "${EXPECTED_VERSION}" ]; then | |
| 52 | + # The version package drops the leading v of a tag: the tag is v0.2.0 and | |
| 53 | + # what a person reads is 0.2.0. | |
| 54 | + want="${EXPECTED_VERSION#v}" | |
| 55 | + if [ "${number}" != "${want}" ]; then | |
| 56 | + echo "check-version: the build meant to stamp ${want} and the binary reports ${number}" >&2 | |
| 57 | + echo " ${reported}" >&2 | |
| 58 | + exit 1 | |
| 59 | + fi | |
| 60 | +elif [ "${number}" = "unknown" ]; then | |
| 61 | + echo "check-version: the binary cannot name its own version" >&2 | |
| 62 | + echo " ${reported}" >&2 | |
| 63 | + exit 1 | |
| 64 | +fi | |
| 65 | + | |
| 66 | +if [ -n "${EXPECTED_COMMIT}" ] && [ "${reported}" = "${reported#*"${EXPECTED_COMMIT}"}" ]; then | |
| 67 | + echo "check-version: the build meant to stamp commit ${EXPECTED_COMMIT} and the binary reports:" >&2 | |
| 68 | + echo " ${reported}" >&2 | |
| 69 | + exit 1 | |
| 70 | +fi | |
| 71 | + | |
| 72 | +echo "${reported}" | |
| new file mode 100755 | |||
| @@ -0,0 +1,72 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +# | ||
| 3 | +# Check that a freshly built binary reports the version the build meant to put | ||
| 4 | +# into it. | ||
| 5 | +# | ||
| 6 | +# scripts/check-version.sh bin/turbo-golo v0.2.0 88a4c38 | ||
| 7 | +# scripts/check-version.sh bin/turbo-golo # unstamped build | ||
| 8 | +# | ||
| 9 | +# Linker flags are a string: a typo in one produces a binary that builds, links | ||
| 10 | +# and runs, and quietly reports the wrong version — or "unknown". Nothing but | ||
| 11 | +# running the binary catches that, so the build runs it. | ||
| 12 | +# | ||
| 13 | +# With a version to expect, the reported number must **equal** it. A substring | ||
| 14 | +# test is not enough: "0.2.0" is a substring of "10.2.0" and of a commit hash | ||
| 15 | +# that happens to contain it, and the case this exists to catch is a stamp that | ||
| 16 | +# is nearly right. | ||
| 17 | +# | ||
| 18 | +# With no version to expect — a build outside a git checkout, where there is | ||
| 19 | +# nothing to describe — the only claim left is that some source named it, so | ||
| 20 | +# "unknown" is the failure. | ||
| 21 | + | ||
| 22 | +set -euo pipefail | ||
| 23 | + | ||
| 24 | +if [ $# -lt 1 ]; then | ||
| 25 | + echo "usage: $0 <binary> [expected-version] [expected-commit]" >&2 | ||
| 26 | + exit 2 | ||
| 27 | +fi | ||
| 28 | + | ||
| 29 | +readonly BINARY="$1" | ||
| 30 | +readonly EXPECTED_VERSION="${2:-}" | ||
| 31 | +readonly EXPECTED_COMMIT="${3:-}" | ||
| 32 | + | ||
| 33 | +if [ ! -x "${BINARY}" ]; then | ||
| 34 | + echo "check-version: ${BINARY} is not an executable file" >&2 | ||
| 35 | + exit 1 | ||
| 36 | +fi | ||
| 37 | + | ||
| 38 | +if ! reported="$("${BINARY}" -version 2>&1)"; then | ||
| 39 | + echo "check-version: ${BINARY} does not run:" >&2 | ||
| 40 | + echo "${reported}" >&2 | ||
| 41 | + exit 1 | ||
| 42 | +fi | ||
| 43 | + | ||
| 44 | +# The binary prints "<Name> <number>" or "<Name> <number> (<commit>, built …)", | ||
| 45 | +# so the number is the last field before the parenthesis, if there is one. The | ||
| 46 | +# name is two words in every editor built on turbo-core and one word in some | ||
| 47 | +# future one, which is why it is read from the right rather than the left. | ||
| 48 | +head="${reported%% (*}" | ||
| 49 | +number="${head##* }" | ||
| 50 | + | ||
| 51 | +if [ -n "${EXPECTED_VERSION}" ]; then | ||
| 52 | + # The version package drops the leading v of a tag: the tag is v0.2.0 and | ||
| 53 | + # what a person reads is 0.2.0. | ||
| 54 | + want="${EXPECTED_VERSION#v}" | ||
| 55 | + if [ "${number}" != "${want}" ]; then | ||
| 56 | + echo "check-version: the build meant to stamp ${want} and the binary reports ${number}" >&2 | ||
| 57 | + echo " ${reported}" >&2 | ||
| 58 | + exit 1 | ||
| 59 | + fi | ||
| 60 | +elif [ "${number}" = "unknown" ]; then | ||
| 61 | + echo "check-version: the binary cannot name its own version" >&2 | ||
| 62 | + echo " ${reported}" >&2 | ||
| 63 | + exit 1 | ||
| 64 | +fi | ||
| 65 | + | ||
| 66 | +if [ -n "${EXPECTED_COMMIT}" ] && [ "${reported}" = "${reported#*"${EXPECTED_COMMIT}"}" ]; then | ||
| 67 | + echo "check-version: the build meant to stamp commit ${EXPECTED_COMMIT} and the binary reports:" >&2 | ||
| 68 | + echo " ${reported}" >&2 | ||
| 69 | + exit 1 | ||
| 70 | +fi | ||
| 71 | + | ||
| 72 | +echo "${reported}" | ||
added
scripts/install.sh +305 -0 | new file mode 100755 | ||
| @@ -0,0 +1,305 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# | |
| 3 | +# Build turbo-golo and install it where your shell can find it. | |
| 4 | +# | |
| 5 | +# scripts/install.sh # install into GOBIN, or GOPATH/bin | |
| 6 | +# scripts/install.sh --prefix ~/bin # install somewhere else | |
| 7 | +# scripts/install.sh --with-server # build and install GoloScript too | |
| 8 | +# scripts/install.sh --uninstall # remove it again | |
| 9 | +# | |
| 10 | +# The build goes to a temporary file first, so a failed build never replaces a | |
| 11 | +# working installation, and the install itself is a rename rather than a write | |
| 12 | +# over the binary that is already there. The version is stamped in by the | |
| 13 | +# linker, so `turbo-golo -version` names the commit it was built from. | |
| 14 | + | |
| 15 | +set -euo pipefail | |
| 16 | + | |
| 17 | +readonly BINARY=turbo-golo | |
| 18 | + | |
| 19 | +# --- output ----------------------------------------------------------------- | |
| 20 | + | |
| 21 | +if [ -t 1 ]; then | |
| 22 | + readonly BOLD=$'\033[1m' DIM=$'\033[2m' RED=$'\033[31m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RESET=$'\033[0m' | |
| 23 | +else | |
| 24 | + readonly BOLD='' DIM='' RED='' GREEN='' YELLOW='' RESET='' | |
| 25 | +fi | |
| 26 | + | |
| 27 | +info() { printf '%s\n' "$*"; } | |
| 28 | +step() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*"; } | |
| 29 | +ok() { printf '%s ✓%s %s\n' "$GREEN" "$RESET" "$*"; } | |
| 30 | +warn() { printf '%s !%s %s\n' "$YELLOW" "$RESET" "$*"; } | |
| 31 | +die() { | |
| 32 | + printf '%s ✗%s %s\n' "$RED" "$RESET" "$*" >&2 | |
| 33 | + exit 1 | |
| 34 | +} | |
| 35 | + | |
| 36 | +usage() { | |
| 37 | + cat <<EOF | |
| 38 | +${BOLD}$BINARY installer${RESET} | |
| 39 | + | |
| 40 | + scripts/install.sh [options] | |
| 41 | + | |
| 42 | +Options: | |
| 43 | + -p, --prefix DIR install into DIR (default: GOBIN, or GOPATH/bin) | |
| 44 | + --with-server also build and install GoloScript — golo, gogolo and | |
| 45 | + wagolo — from source, which completion needs | |
| 46 | + --uninstall remove an installed $BINARY and stop | |
| 47 | + -h, --help show this and stop | |
| 48 | +EOF | |
| 49 | +} | |
| 50 | + | |
| 51 | +# --- arguments -------------------------------------------------------------- | |
| 52 | + | |
| 53 | +prefix="" | |
| 54 | +with_server=false | |
| 55 | +uninstall=false | |
| 56 | + | |
| 57 | +while [ $# -gt 0 ]; do | |
| 58 | + case "$1" in | |
| 59 | + -p | --prefix) | |
| 60 | + [ $# -ge 2 ] || die "--prefix needs a directory" | |
| 61 | + prefix="$2" | |
| 62 | + shift 2 | |
| 63 | + ;; | |
| 64 | + --with-server) | |
| 65 | + with_server=true | |
| 66 | + shift | |
| 67 | + ;; | |
| 68 | + --uninstall) | |
| 69 | + uninstall=true | |
| 70 | + shift | |
| 71 | + ;; | |
| 72 | + -h | --help) | |
| 73 | + usage | |
| 74 | + exit 0 | |
| 75 | + ;; | |
| 76 | + *) die "unknown option: $1 (try --help)" ;; | |
| 77 | + esac | |
| 78 | +done | |
| 79 | + | |
| 80 | +# --- where things are ------------------------------------------------------- | |
| 81 | + | |
| 82 | +cd "$(dirname "${BASH_SOURCE[0]}")/.." | |
| 83 | +readonly REPO="$PWD" | |
| 84 | + | |
| 85 | +command -v go >/dev/null 2>&1 || die "go is not installed: https://go.dev/dl/" | |
| 86 | + | |
| 87 | +# default_prefix returns where "go install" would put a binary: GOBIN when it | |
| 88 | +# is set, GOPATH/bin otherwise. That is the directory a Go developer is most | |
| 89 | +# likely to already have on PATH. | |
| 90 | +default_prefix() { | |
| 91 | + local gobin | |
| 92 | + gobin="$(go env GOBIN)" | |
| 93 | + if [ -n "$gobin" ]; then | |
| 94 | + printf '%s\n' "$gobin" | |
| 95 | + else | |
| 96 | + printf '%s/bin\n' "$(go env GOPATH)" | |
| 97 | + fi | |
| 98 | +} | |
| 99 | + | |
| 100 | +[ -n "$prefix" ] || prefix="$(default_prefix)" | |
| 101 | +[ -n "$prefix" ] || die "cannot work out where to install; use --prefix DIR" | |
| 102 | +readonly TARGET="$prefix/$BINARY" | |
| 103 | + | |
| 104 | +# --- uninstall -------------------------------------------------------------- | |
| 105 | + | |
| 106 | +if $uninstall; then | |
| 107 | + step "Removing $TARGET" | |
| 108 | + if [ -e "$TARGET" ]; then | |
| 109 | + rm -f "$TARGET" | |
| 110 | + ok "removed" | |
| 111 | + else | |
| 112 | + warn "nothing installed at $TARGET" | |
| 113 | + fi | |
| 114 | + exit 0 | |
| 115 | +fi | |
| 116 | + | |
| 117 | +# --- toolchain -------------------------------------------------------------- | |
| 118 | + | |
| 119 | +step "Checking the Go toolchain" | |
| 120 | + | |
| 121 | +# The requirement lives in go.mod, so this check cannot drift from the build. | |
| 122 | +required="$(awk '/^go /{print $2; exit}' "$REPO/go.mod")" | |
| 123 | +installed="$(go env GOVERSION)" | |
| 124 | +installed="${installed#go}" | |
| 125 | + | |
| 126 | +if [ "$(printf '%s\n%s\n' "$required" "$installed" | sort -V | head -1)" != "$required" ]; then | |
| 127 | + die "Go $required or later is needed, but $installed is installed" | |
| 128 | +fi | |
| 129 | +ok "go $installed (go.mod asks for $required or later)" | |
| 130 | + | |
| 131 | +# --- build ------------------------------------------------------------------ | |
| 132 | + | |
| 133 | +step "Building $BINARY" | |
| 134 | + | |
| 135 | +readonly STAGING="$(mktemp -d)" | |
| 136 | +trap 'rm -rf "$STAGING"' EXIT | |
| 137 | + | |
| 138 | +# The version the binary reports is stamped in by the linker, so that an | |
| 139 | +# installed editor names the commit it was actually built from rather than a | |
| 140 | +# constant somebody forgot to bump. Outside a git checkout — installed from a | |
| 141 | +# tarball, say — there is nothing to describe and the binary works the version | |
| 142 | +# out from its own build information instead. | |
| 143 | +version_pkg="rickub.com/turbo-editors/turbo-core/version" | |
| 144 | +if describe="$(git -C "$REPO" describe --tags --dirty 2>/dev/null)"; then | |
| 145 | + commit="$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || true)" | |
| 146 | + built="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| 147 | + ldflags="-X '$version_pkg.stamp=$describe' -X '$version_pkg.commit=$commit' -X '$version_pkg.built=$built'" | |
| 148 | +else | |
| 149 | + ldflags="" | |
| 150 | +fi | |
| 151 | + | |
| 152 | +if ! go build -ldflags "$ldflags" -o "$STAGING/$BINARY" "$REPO" 2>"$STAGING/build.log"; then | |
| 153 | + cat "$STAGING/build.log" >&2 | |
| 154 | + info "" | |
| 155 | + warn "A failure in the repository root is often a stray .go file that has" | |
| 156 | + warn "landed in package main. 'go vet .' names it." | |
| 157 | + die "build failed; nothing was installed" | |
| 158 | +fi | |
| 159 | + | |
| 160 | +# Running the staged binary is the only proof that the flags above reached the | |
| 161 | +# linker rather than merely looking right. It happens before the install, so a | |
| 162 | +# binary that cannot name its own version never replaces a working one. | |
| 163 | +if ! stamped="$("$REPO/scripts/check-version.sh" "$STAGING/$BINARY" "${describe:-}" "${commit:-}" 2>&1)"; then | |
| 164 | + info "$stamped" | |
| 165 | + die "the build did not carry its version; nothing was installed" | |
| 166 | +fi | |
| 167 | +ok "built — $stamped" | |
| 168 | + | |
| 169 | +# --- install ---------------------------------------------------------------- | |
| 170 | + | |
| 171 | +step "Installing into $prefix" | |
| 172 | + | |
| 173 | +mkdir -p "$prefix" || die "cannot create $prefix" | |
| 174 | + | |
| 175 | +# Install by renaming a complete file over the target, never by writing into | |
| 176 | +# the one that is there. | |
| 177 | +# | |
| 178 | +# macOS caches a binary's code signature against its inode. cp writes new bytes | |
| 179 | +# into the existing inode, so the cached signature ends up describing something | |
| 180 | +# else and the kernel refuses to execute the result — a reinstall that builds, | |
| 181 | +# installs, and then will not run. A rename gives the name a fresh inode, so | |
| 182 | +# there is nothing stale to cache. It is atomic besides: no moment at which a | |
| 183 | +# half-written turbo-golo is on the PATH. | |
| 184 | +# | |
| 185 | +# The temporary has to sit in $prefix, because a rename only works within one | |
| 186 | +# filesystem and $STAGING is somewhere else entirely. | |
| 187 | +readonly INCOMING="$prefix/.$BINARY.incoming.$$" | |
| 188 | +trap 'rm -rf "$STAGING"; rm -f "$INCOMING"' EXIT | |
| 189 | + | |
| 190 | +cp "$STAGING/$BINARY" "$INCOMING" || die "cannot write into $prefix" | |
| 191 | +chmod 0755 "$INCOMING" | |
| 192 | +mv -f "$INCOMING" "$TARGET" || die "cannot replace $TARGET" | |
| 193 | + | |
| 194 | +# Whatever the system said is the useful part: "does not run" on its own tells | |
| 195 | +# nobody anything they can act on. | |
| 196 | +if ! verify="$("$TARGET" -version 2>&1)"; then | |
| 197 | + info "$verify" | |
| 198 | + die "the installed binary does not run" | |
| 199 | +fi | |
| 200 | +version="$verify" | |
| 201 | +ok "$version → $TARGET" | |
| 202 | + | |
| 203 | +# --- PATH ------------------------------------------------------------------- | |
| 204 | + | |
| 205 | +# on_path reports whether a directory is one the shell searches. | |
| 206 | +on_path() { | |
| 207 | + case ":${PATH:-}:" in | |
| 208 | + *":$1:"*) return 0 ;; | |
| 209 | + *) return 1 ;; | |
| 210 | + esac | |
| 211 | +} | |
| 212 | + | |
| 213 | +# shell_profile guesses the file that sets PATH for the user's shell. | |
| 214 | +shell_profile() { | |
| 215 | + case "${SHELL:-}" in | |
| 216 | + */zsh) printf '~/.zshrc\n' ;; | |
| 217 | + */fish) printf '~/.config/fish/config.fish\n' ;; | |
| 218 | + *) printf '~/.bashrc\n' ;; | |
| 219 | + esac | |
| 220 | +} | |
| 221 | + | |
| 222 | +step "Checking your PATH" | |
| 223 | +if on_path "$prefix"; then | |
| 224 | + ok "$prefix is on your PATH" | |
| 225 | +else | |
| 226 | + warn "$prefix is not on your PATH. Add it:" | |
| 227 | + info "" | |
| 228 | + info " echo 'export PATH=\"\$PATH:$prefix\"' >> $(shell_profile)" | |
| 229 | + info " exec \$SHELL" | |
| 230 | +fi | |
| 231 | + | |
| 232 | +# --- the language server ---------------------------------------------------- | |
| 233 | + | |
| 234 | +# The language server is the interpreter itself. `golo lsp` puts the same | |
| 235 | +# binary that runs a script into language-server mode, so a machine that can | |
| 236 | +# run Golo can complete Golo, and there is nothing separate to install. | |
| 237 | +readonly SERVER=golo | |
| 238 | +readonly GOLOSCRIPT_REPO=https://codeberg.org/TypeUnsafe/golo-script | |
| 239 | +readonly GOLOSCRIPT_RELEASES="$GOLOSCRIPT_REPO/releases" | |
| 240 | + | |
| 241 | +# find_server looks where the editor itself looks: PATH, then /usr/local/bin, | |
| 242 | +# which is where GoloScript's own install.sh writes. | |
| 243 | +# | |
| 244 | +# Finding it is not the same as its working: a file left behind by a half-undone | |
| 245 | +# installation sits on PATH and fails only when started. So this asks it for its | |
| 246 | +# version rather than trusting the file's existence, which is the difference | |
| 247 | +# between "you have completion" and "you will find out you have not when you | |
| 248 | +# press Ctrl-Space". | |
| 249 | +find_server() { | |
| 250 | + local candidate | |
| 251 | + for candidate in \ | |
| 252 | + "$(command -v "$SERVER" 2>/dev/null || true)" \ | |
| 253 | + "/usr/local/bin/$SERVER"; do | |
| 254 | + [ -n "$candidate" ] && [ -x "$candidate" ] || continue | |
| 255 | + "$candidate" --version >/dev/null 2>&1 || continue | |
| 256 | + printf '%s\n' "$candidate" | |
| 257 | + return 0 | |
| 258 | + done | |
| 259 | + return 1 | |
| 260 | +} | |
| 261 | + | |
| 262 | +# install_server builds GoloScript from source and runs its own installer, | |
| 263 | +# which puts golo, gogolo and wagolo into /usr/local/bin. It needs git and go, | |
| 264 | +# and the installer asks for sudo when it copies. | |
| 265 | +install_server() { | |
| 266 | + command -v git >/dev/null 2>&1 || die "git is needed to fetch GoloScript" | |
| 267 | + local checkout | |
| 268 | + checkout="$(mktemp -d)" | |
| 269 | + git clone --depth 1 "$GOLOSCRIPT_REPO.git" "$checkout" || | |
| 270 | + die "cloning GoloScript failed; precompiled binaries are at $GOLOSCRIPT_RELEASES" | |
| 271 | + (cd "$checkout" && ./install.sh) || | |
| 272 | + die "GoloScript's installer failed; precompiled binaries are at $GOLOSCRIPT_RELEASES" | |
| 273 | + rm -rf "$checkout" | |
| 274 | +} | |
| 275 | + | |
| 276 | +step "Checking the language server" | |
| 277 | + | |
| 278 | +if $with_server && ! find_server >/dev/null; then | |
| 279 | + info " building GoloScript from source…" | |
| 280 | + install_server | |
| 281 | +fi | |
| 282 | + | |
| 283 | +if server_path="$(find_server)"; then | |
| 284 | + ok "$SERVER at $server_path — completion comes from \`golo lsp\`" | |
| 285 | +else | |
| 286 | + warn "$SERVER is not installed, so there will be no completion." | |
| 287 | + warn "Editing, colouring and themes all work without it." | |
| 288 | + info "" | |
| 289 | + info " a binary for your platform: $GOLOSCRIPT_RELEASES" | |
| 290 | + info " ${DIM}or re-run this script with --with-server to build it from source${RESET}" | |
| 291 | +fi | |
| 292 | + | |
| 293 | +# --- what to do next -------------------------------------------------------- | |
| 294 | + | |
| 295 | +info "" | |
| 296 | +step "Ready" | |
| 297 | +info "" | |
| 298 | +info " Open a Golo script:" | |
| 299 | +info "" | |
| 300 | +info " cd /path/to/your/scripts" | |
| 301 | +info " $BINARY main.golo" | |
| 302 | +info "" | |
| 303 | +info " ${DIM}F10 menu · F2 save · Alt-X exit · Ctrl-Space for completion${RESET}" | |
| 304 | +info " ${DIM}themes: $BINARY -list-themes${RESET}" | |
| 305 | +info "" | |
| new file mode 100755 | |||
| @@ -0,0 +1,305 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +# | ||
| 3 | +# Build turbo-golo and install it where your shell can find it. | ||
| 4 | +# | ||
| 5 | +# scripts/install.sh # install into GOBIN, or GOPATH/bin | ||
| 6 | +# scripts/install.sh --prefix ~/bin # install somewhere else | ||
| 7 | +# scripts/install.sh --with-server # build and install GoloScript too | ||
| 8 | +# scripts/install.sh --uninstall # remove it again | ||
| 9 | +# | ||
| 10 | +# The build goes to a temporary file first, so a failed build never replaces a | ||
| 11 | +# working installation, and the install itself is a rename rather than a write | ||
| 12 | +# over the binary that is already there. The version is stamped in by the | ||
| 13 | +# linker, so `turbo-golo -version` names the commit it was built from. | ||
| 14 | + | ||
| 15 | +set -euo pipefail | ||
| 16 | + | ||
| 17 | +readonly BINARY=turbo-golo | ||
| 18 | + | ||
| 19 | +# --- output ----------------------------------------------------------------- | ||
| 20 | + | ||
| 21 | +if [ -t 1 ]; then | ||
| 22 | + readonly BOLD=$'\033[1m' DIM=$'\033[2m' RED=$'\033[31m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RESET=$'\033[0m' | ||
| 23 | +else | ||
| 24 | + readonly BOLD='' DIM='' RED='' GREEN='' YELLOW='' RESET='' | ||
| 25 | +fi | ||
| 26 | + | ||
| 27 | +info() { printf '%s\n' "$*"; } | ||
| 28 | +step() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*"; } | ||
| 29 | +ok() { printf '%s ✓%s %s\n' "$GREEN" "$RESET" "$*"; } | ||
| 30 | +warn() { printf '%s !%s %s\n' "$YELLOW" "$RESET" "$*"; } | ||
| 31 | +die() { | ||
| 32 | + printf '%s ✗%s %s\n' "$RED" "$RESET" "$*" >&2 | ||
| 33 | + exit 1 | ||
| 34 | +} | ||
| 35 | + | ||
| 36 | +usage() { | ||
| 37 | + cat <<EOF | ||
| 38 | +${BOLD}$BINARY installer${RESET} | ||
| 39 | + | ||
| 40 | + scripts/install.sh [options] | ||
| 41 | + | ||
| 42 | +Options: | ||
| 43 | + -p, --prefix DIR install into DIR (default: GOBIN, or GOPATH/bin) | ||
| 44 | + --with-server also build and install GoloScript — golo, gogolo and | ||
| 45 | + wagolo — from source, which completion needs | ||
| 46 | + --uninstall remove an installed $BINARY and stop | ||
| 47 | + -h, --help show this and stop | ||
| 48 | +EOF | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +# --- arguments -------------------------------------------------------------- | ||
| 52 | + | ||
| 53 | +prefix="" | ||
| 54 | +with_server=false | ||
| 55 | +uninstall=false | ||
| 56 | + | ||
| 57 | +while [ $# -gt 0 ]; do | ||
| 58 | + case "$1" in | ||
| 59 | + -p | --prefix) | ||
| 60 | + [ $# -ge 2 ] || die "--prefix needs a directory" | ||
| 61 | + prefix="$2" | ||
| 62 | + shift 2 | ||
| 63 | + ;; | ||
| 64 | + --with-server) | ||
| 65 | + with_server=true | ||
| 66 | + shift | ||
| 67 | + ;; | ||
| 68 | + --uninstall) | ||
| 69 | + uninstall=true | ||
| 70 | + shift | ||
| 71 | + ;; | ||
| 72 | + -h | --help) | ||
| 73 | + usage | ||
| 74 | + exit 0 | ||
| 75 | + ;; | ||
| 76 | + *) die "unknown option: $1 (try --help)" ;; | ||
| 77 | + esac | ||
| 78 | +done | ||
| 79 | + | ||
| 80 | +# --- where things are ------------------------------------------------------- | ||
| 81 | + | ||
| 82 | +cd "$(dirname "${BASH_SOURCE[0]}")/.." | ||
| 83 | +readonly REPO="$PWD" | ||
| 84 | + | ||
| 85 | +command -v go >/dev/null 2>&1 || die "go is not installed: https://go.dev/dl/" | ||
| 86 | + | ||
| 87 | +# default_prefix returns where "go install" would put a binary: GOBIN when it | ||
| 88 | +# is set, GOPATH/bin otherwise. That is the directory a Go developer is most | ||
| 89 | +# likely to already have on PATH. | ||
| 90 | +default_prefix() { | ||
| 91 | + local gobin | ||
| 92 | + gobin="$(go env GOBIN)" | ||
| 93 | + if [ -n "$gobin" ]; then | ||
| 94 | + printf '%s\n' "$gobin" | ||
| 95 | + else | ||
| 96 | + printf '%s/bin\n' "$(go env GOPATH)" | ||
| 97 | + fi | ||
| 98 | +} | ||
| 99 | + | ||
| 100 | +[ -n "$prefix" ] || prefix="$(default_prefix)" | ||
| 101 | +[ -n "$prefix" ] || die "cannot work out where to install; use --prefix DIR" | ||
| 102 | +readonly TARGET="$prefix/$BINARY" | ||
| 103 | + | ||
| 104 | +# --- uninstall -------------------------------------------------------------- | ||
| 105 | + | ||
| 106 | +if $uninstall; then | ||
| 107 | + step "Removing $TARGET" | ||
| 108 | + if [ -e "$TARGET" ]; then | ||
| 109 | + rm -f "$TARGET" | ||
| 110 | + ok "removed" | ||
| 111 | + else | ||
| 112 | + warn "nothing installed at $TARGET" | ||
| 113 | + fi | ||
| 114 | + exit 0 | ||
| 115 | +fi | ||
| 116 | + | ||
| 117 | +# --- toolchain -------------------------------------------------------------- | ||
| 118 | + | ||
| 119 | +step "Checking the Go toolchain" | ||
| 120 | + | ||
| 121 | +# The requirement lives in go.mod, so this check cannot drift from the build. | ||
| 122 | +required="$(awk '/^go /{print $2; exit}' "$REPO/go.mod")" | ||
| 123 | +installed="$(go env GOVERSION)" | ||
| 124 | +installed="${installed#go}" | ||
| 125 | + | ||
| 126 | +if [ "$(printf '%s\n%s\n' "$required" "$installed" | sort -V | head -1)" != "$required" ]; then | ||
| 127 | + die "Go $required or later is needed, but $installed is installed" | ||
| 128 | +fi | ||
| 129 | +ok "go $installed (go.mod asks for $required or later)" | ||
| 130 | + | ||
| 131 | +# --- build ------------------------------------------------------------------ | ||
| 132 | + | ||
| 133 | +step "Building $BINARY" | ||
| 134 | + | ||
| 135 | +readonly STAGING="$(mktemp -d)" | ||
| 136 | +trap 'rm -rf "$STAGING"' EXIT | ||
| 137 | + | ||
| 138 | +# The version the binary reports is stamped in by the linker, so that an | ||
| 139 | +# installed editor names the commit it was actually built from rather than a | ||
| 140 | +# constant somebody forgot to bump. Outside a git checkout — installed from a | ||
| 141 | +# tarball, say — there is nothing to describe and the binary works the version | ||
| 142 | +# out from its own build information instead. | ||
| 143 | +version_pkg="rickub.com/turbo-editors/turbo-core/version" | ||
| 144 | +if describe="$(git -C "$REPO" describe --tags --dirty 2>/dev/null)"; then | ||
| 145 | + commit="$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || true)" | ||
| 146 | + built="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | ||
| 147 | + ldflags="-X '$version_pkg.stamp=$describe' -X '$version_pkg.commit=$commit' -X '$version_pkg.built=$built'" | ||
| 148 | +else | ||
| 149 | + ldflags="" | ||
| 150 | +fi | ||
| 151 | + | ||
| 152 | +if ! go build -ldflags "$ldflags" -o "$STAGING/$BINARY" "$REPO" 2>"$STAGING/build.log"; then | ||
| 153 | + cat "$STAGING/build.log" >&2 | ||
| 154 | + info "" | ||
| 155 | + warn "A failure in the repository root is often a stray .go file that has" | ||
| 156 | + warn "landed in package main. 'go vet .' names it." | ||
| 157 | + die "build failed; nothing was installed" | ||
| 158 | +fi | ||
| 159 | + | ||
| 160 | +# Running the staged binary is the only proof that the flags above reached the | ||
| 161 | +# linker rather than merely looking right. It happens before the install, so a | ||
| 162 | +# binary that cannot name its own version never replaces a working one. | ||
| 163 | +if ! stamped="$("$REPO/scripts/check-version.sh" "$STAGING/$BINARY" "${describe:-}" "${commit:-}" 2>&1)"; then | ||
| 164 | + info "$stamped" | ||
| 165 | + die "the build did not carry its version; nothing was installed" | ||
| 166 | +fi | ||
| 167 | +ok "built — $stamped" | ||
| 168 | + | ||
| 169 | +# --- install ---------------------------------------------------------------- | ||
| 170 | + | ||
| 171 | +step "Installing into $prefix" | ||
| 172 | + | ||
| 173 | +mkdir -p "$prefix" || die "cannot create $prefix" | ||
| 174 | + | ||
| 175 | +# Install by renaming a complete file over the target, never by writing into | ||
| 176 | +# the one that is there. | ||
| 177 | +# | ||
| 178 | +# macOS caches a binary's code signature against its inode. cp writes new bytes | ||
| 179 | +# into the existing inode, so the cached signature ends up describing something | ||
| 180 | +# else and the kernel refuses to execute the result — a reinstall that builds, | ||
| 181 | +# installs, and then will not run. A rename gives the name a fresh inode, so | ||
| 182 | +# there is nothing stale to cache. It is atomic besides: no moment at which a | ||
| 183 | +# half-written turbo-golo is on the PATH. | ||
| 184 | +# | ||
| 185 | +# The temporary has to sit in $prefix, because a rename only works within one | ||
| 186 | +# filesystem and $STAGING is somewhere else entirely. | ||
| 187 | +readonly INCOMING="$prefix/.$BINARY.incoming.$$" | ||
| 188 | +trap 'rm -rf "$STAGING"; rm -f "$INCOMING"' EXIT | ||
| 189 | + | ||
| 190 | +cp "$STAGING/$BINARY" "$INCOMING" || die "cannot write into $prefix" | ||
| 191 | +chmod 0755 "$INCOMING" | ||
| 192 | +mv -f "$INCOMING" "$TARGET" || die "cannot replace $TARGET" | ||
| 193 | + | ||
| 194 | +# Whatever the system said is the useful part: "does not run" on its own tells | ||
| 195 | +# nobody anything they can act on. | ||
| 196 | +if ! verify="$("$TARGET" -version 2>&1)"; then | ||
| 197 | + info "$verify" | ||
| 198 | + die "the installed binary does not run" | ||
| 199 | +fi | ||
| 200 | +version="$verify" | ||
| 201 | +ok "$version → $TARGET" | ||
| 202 | + | ||
| 203 | +# --- PATH ------------------------------------------------------------------- | ||
| 204 | + | ||
| 205 | +# on_path reports whether a directory is one the shell searches. | ||
| 206 | +on_path() { | ||
| 207 | + case ":${PATH:-}:" in | ||
| 208 | + *":$1:"*) return 0 ;; | ||
| 209 | + *) return 1 ;; | ||
| 210 | + esac | ||
| 211 | +} | ||
| 212 | + | ||
| 213 | +# shell_profile guesses the file that sets PATH for the user's shell. | ||
| 214 | +shell_profile() { | ||
| 215 | + case "${SHELL:-}" in | ||
| 216 | + */zsh) printf '~/.zshrc\n' ;; | ||
| 217 | + */fish) printf '~/.config/fish/config.fish\n' ;; | ||
| 218 | + *) printf '~/.bashrc\n' ;; | ||
| 219 | + esac | ||
| 220 | +} | ||
| 221 | + | ||
| 222 | +step "Checking your PATH" | ||
| 223 | +if on_path "$prefix"; then | ||
| 224 | + ok "$prefix is on your PATH" | ||
| 225 | +else | ||
| 226 | + warn "$prefix is not on your PATH. Add it:" | ||
| 227 | + info "" | ||
| 228 | + info " echo 'export PATH=\"\$PATH:$prefix\"' >> $(shell_profile)" | ||
| 229 | + info " exec \$SHELL" | ||
| 230 | +fi | ||
| 231 | + | ||
| 232 | +# --- the language server ---------------------------------------------------- | ||
| 233 | + | ||
| 234 | +# The language server is the interpreter itself. `golo lsp` puts the same | ||
| 235 | +# binary that runs a script into language-server mode, so a machine that can | ||
| 236 | +# run Golo can complete Golo, and there is nothing separate to install. | ||
| 237 | +readonly SERVER=golo | ||
| 238 | +readonly GOLOSCRIPT_REPO=https://codeberg.org/TypeUnsafe/golo-script | ||
| 239 | +readonly GOLOSCRIPT_RELEASES="$GOLOSCRIPT_REPO/releases" | ||
| 240 | + | ||
| 241 | +# find_server looks where the editor itself looks: PATH, then /usr/local/bin, | ||
| 242 | +# which is where GoloScript's own install.sh writes. | ||
| 243 | +# | ||
| 244 | +# Finding it is not the same as its working: a file left behind by a half-undone | ||
| 245 | +# installation sits on PATH and fails only when started. So this asks it for its | ||
| 246 | +# version rather than trusting the file's existence, which is the difference | ||
| 247 | +# between "you have completion" and "you will find out you have not when you | ||
| 248 | +# press Ctrl-Space". | ||
| 249 | +find_server() { | ||
| 250 | + local candidate | ||
| 251 | + for candidate in \ | ||
| 252 | + "$(command -v "$SERVER" 2>/dev/null || true)" \ | ||
| 253 | + "/usr/local/bin/$SERVER"; do | ||
| 254 | + [ -n "$candidate" ] && [ -x "$candidate" ] || continue | ||
| 255 | + "$candidate" --version >/dev/null 2>&1 || continue | ||
| 256 | + printf '%s\n' "$candidate" | ||
| 257 | + return 0 | ||
| 258 | + done | ||
| 259 | + return 1 | ||
| 260 | +} | ||
| 261 | + | ||
| 262 | +# install_server builds GoloScript from source and runs its own installer, | ||
| 263 | +# which puts golo, gogolo and wagolo into /usr/local/bin. It needs git and go, | ||
| 264 | +# and the installer asks for sudo when it copies. | ||
| 265 | +install_server() { | ||
| 266 | + command -v git >/dev/null 2>&1 || die "git is needed to fetch GoloScript" | ||
| 267 | + local checkout | ||
| 268 | + checkout="$(mktemp -d)" | ||
| 269 | + git clone --depth 1 "$GOLOSCRIPT_REPO.git" "$checkout" || | ||
| 270 | + die "cloning GoloScript failed; precompiled binaries are at $GOLOSCRIPT_RELEASES" | ||
| 271 | + (cd "$checkout" && ./install.sh) || | ||
| 272 | + die "GoloScript's installer failed; precompiled binaries are at $GOLOSCRIPT_RELEASES" | ||
| 273 | + rm -rf "$checkout" | ||
| 274 | +} | ||
| 275 | + | ||
| 276 | +step "Checking the language server" | ||
| 277 | + | ||
| 278 | +if $with_server && ! find_server >/dev/null; then | ||
| 279 | + info " building GoloScript from source…" | ||
| 280 | + install_server | ||
| 281 | +fi | ||
| 282 | + | ||
| 283 | +if server_path="$(find_server)"; then | ||
| 284 | + ok "$SERVER at $server_path — completion comes from \`golo lsp\`" | ||
| 285 | +else | ||
| 286 | + warn "$SERVER is not installed, so there will be no completion." | ||
| 287 | + warn "Editing, colouring and themes all work without it." | ||
| 288 | + info "" | ||
| 289 | + info " a binary for your platform: $GOLOSCRIPT_RELEASES" | ||
| 290 | + info " ${DIM}or re-run this script with --with-server to build it from source${RESET}" | ||
| 291 | +fi | ||
| 292 | + | ||
| 293 | +# --- what to do next -------------------------------------------------------- | ||
| 294 | + | ||
| 295 | +info "" | ||
| 296 | +step "Ready" | ||
| 297 | +info "" | ||
| 298 | +info " Open a Golo script:" | ||
| 299 | +info "" | ||
| 300 | +info " cd /path/to/your/scripts" | ||
| 301 | +info " $BINARY main.golo" | ||
| 302 | +info "" | ||
| 303 | +info " ${DIM}F10 menu · F2 save · Alt-X exit · Ctrl-Space for completion${RESET}" | ||
| 304 | +info " ${DIM}themes: $BINARY -list-themes${RESET}" | ||
| 305 | +info "" | ||
added
version_check_test.go +174 -0 | new file mode 100644 | ||
| @@ -0,0 +1,174 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "os" | |
| 5 | + "os/exec" | |
| 6 | + "path/filepath" | |
| 7 | + "strings" | |
| 8 | + "testing" | |
| 9 | +) | |
| 10 | + | |
| 11 | +// Linker flags are a string, and a wrong one is not an error: `-X` naming a | |
| 12 | +// symbol that does not exist links happily and stamps nothing, so the binary | |
| 13 | +// falls back to whatever the Go build system knows and reports a version the | |
| 14 | +// build never meant. Nothing but running the binary catches that, which is why | |
| 15 | +// `make build` runs it and why these tests do too. | |
| 16 | + | |
| 17 | +// buildStampedWith compiles the editor with the flags `make ldflags` produces | |
| 18 | +// for a version, and returns the path to the binary. | |
| 19 | +func buildStampedWith(t *testing.T, version string) string { | |
| 20 | + t.Helper() | |
| 21 | + | |
| 22 | + flags, err := exec.Command("make", "--no-print-directory", "ldflags", "VERSION="+version).Output() | |
| 23 | + if err != nil { | |
| 24 | + t.Fatalf("make ldflags VERSION=%s: %v", version, err) | |
| 25 | + } | |
| 26 | + | |
| 27 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | |
| 28 | + build := exec.Command("go", "build", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".") | |
| 29 | + if out, err := build.CombinedOutput(); err != nil { | |
| 30 | + t.Fatalf("building with those flags failed: %v\n%s", err, out) | |
| 31 | + } | |
| 32 | + return binary | |
| 33 | +} | |
| 34 | + | |
| 35 | +// checkVersion runs the build's version check and returns what it said and | |
| 36 | +// whether it was satisfied. | |
| 37 | +func checkVersion(t *testing.T, args ...string) (string, bool) { | |
| 38 | + t.Helper() | |
| 39 | + | |
| 40 | + out, err := exec.Command("./scripts/check-version.sh", args...).CombinedOutput() | |
| 41 | + return string(out), err == nil | |
| 42 | +} | |
| 43 | + | |
| 44 | +func TestTheVersionCheckAcceptsTheVersionTheBuildStamped(t *testing.T) { | |
| 45 | + binary := buildStampedWith(t, "v9.9.9") | |
| 46 | + | |
| 47 | + out, ok := checkVersion(t, binary, "v9.9.9") | |
| 48 | + if !ok { | |
| 49 | + t.Fatalf("the check refused a correctly stamped binary:\n%s", out) | |
| 50 | + } | |
| 51 | + if !strings.Contains(out, "9.9.9") { | |
| 52 | + t.Errorf("the check reported %q, want it to name the version", out) | |
| 53 | + } | |
| 54 | +} | |
| 55 | + | |
| 56 | +func TestTheVersionCheckRefusesAVersionThatMerelyContainsTheRightOne(t *testing.T) { | |
| 57 | + // The reason this is an equality test and not a grep: "0.2.0" is a | |
| 58 | + // substring of "10.2.0", so a substring check passes a release that ships | |
| 59 | + // a binary naming an entirely different version. | |
| 60 | + binary := buildStampedWith(t, "v10.2.0") | |
| 61 | + | |
| 62 | + out, ok := checkVersion(t, binary, "v0.2.0") | |
| 63 | + if ok { | |
| 64 | + t.Fatalf("the check accepted 10.2.0 as 0.2.0:\n%s", out) | |
| 65 | + } | |
| 66 | + if !strings.Contains(out, "10.2.0") { | |
| 67 | + t.Errorf("the failure does not say what the binary actually reports:\n%s", out) | |
| 68 | + } | |
| 69 | +} | |
| 70 | + | |
| 71 | +func TestTheVersionCheckRefusesAStampThatNeverReachedTheLinker(t *testing.T) { | |
| 72 | + // The failure this exists for. `-X` naming a symbol that is not there is | |
| 73 | + // not an error: the binary links, runs, and reports the wrong thing. | |
| 74 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | |
| 75 | + build := exec.Command("go", "build", | |
| 76 | + "-ldflags", "-X 'rickub.com/turbo-editors/turbo-core/version.stampX=v9.9.9'", | |
| 77 | + "-o", binary, ".") | |
| 78 | + if out, err := build.CombinedOutput(); err != nil { | |
| 79 | + t.Fatalf("the build with a misspelt -X failed, so there is nothing to catch: %v\n%s", err, out) | |
| 80 | + } | |
| 81 | + | |
| 82 | + out, ok := checkVersion(t, binary, "v9.9.9") | |
| 83 | + if ok { | |
| 84 | + t.Fatalf("the check accepted a binary nothing was stamped into:\n%s", out) | |
| 85 | + } | |
| 86 | +} | |
| 87 | + | |
| 88 | +func TestTheVersionCheckRefusesABinaryThatDoesNotRun(t *testing.T) { | |
| 89 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | |
| 90 | + if err := os.WriteFile(binary, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil { | |
| 91 | + t.Fatalf("cannot write the stand-in: %v", err) | |
| 92 | + } | |
| 93 | + | |
| 94 | + if out, ok := checkVersion(t, binary, "v9.9.9"); ok { | |
| 95 | + t.Fatalf("the check accepted a binary that does not run:\n%s", out) | |
| 96 | + } | |
| 97 | +} | |
| 98 | + | |
| 99 | +func TestTheVersionCheckNeedsSomethingToCheck(t *testing.T) { | |
| 100 | + if out, ok := checkVersion(t); ok { | |
| 101 | + t.Fatalf("the check accepted no arguments at all:\n%s", out) | |
| 102 | + } | |
| 103 | + if out, ok := checkVersion(t, filepath.Join(t.TempDir(), "not-there")); ok { | |
| 104 | + t.Fatalf("the check accepted a path with no binary at it:\n%s", out) | |
| 105 | + } | |
| 106 | +} | |
| 107 | + | |
| 108 | +func TestAnUnstampedBuildIsAcceptedButAnUnnameableOneIsNot(t *testing.T) { | |
| 109 | + // Installing from a tarball has no git checkout to describe, so there is no | |
| 110 | + // version to expect. The claim left is that *some* source named it. | |
| 111 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | |
| 112 | + build := exec.Command("go", "build", "-o", binary, ".") | |
| 113 | + if out, err := build.CombinedOutput(); err != nil { | |
| 114 | + t.Fatalf("an unstamped build failed: %v\n%s", err, out) | |
| 115 | + } | |
| 116 | + | |
| 117 | + out, ok := checkVersion(t, binary) | |
| 118 | + if !ok { | |
| 119 | + t.Fatalf("the check refused an unstamped build, which is a legitimate one:\n%s", out) | |
| 120 | + } | |
| 121 | + if strings.Contains(out, "unknown") { | |
| 122 | + t.Errorf("the binary cannot name its version and the check passed anyway:\n%s", out) | |
| 123 | + } | |
| 124 | +} | |
| 125 | + | |
| 126 | +func TestTheBuildTargetChecksWhatItStamped(t *testing.T) { | |
| 127 | + // The wiring, not the script: a check nothing calls protects nothing. | |
| 128 | + makefile, err := os.ReadFile("Makefile") | |
| 129 | + if err != nil { | |
| 130 | + t.Fatalf("cannot read the Makefile: %v", err) | |
| 131 | + } | |
| 132 | + recipe := buildRecipe(t, string(makefile)) | |
| 133 | + | |
| 134 | + if !strings.Contains(recipe, "check-version.sh") { | |
| 135 | + t.Errorf("the build target never checks the version it stamped:\n%s", recipe) | |
| 136 | + } | |
| 137 | +} | |
| 138 | + | |
| 139 | +func TestTheInstallerChecksWhatItStampedBeforeItInstalls(t *testing.T) { | |
| 140 | + // Before, not after: a binary that cannot name its own version must never | |
| 141 | + // replace one that can. | |
| 142 | + script, err := os.ReadFile("scripts/install.sh") | |
| 143 | + if err != nil { | |
| 144 | + t.Fatalf("cannot read the installer: %v", err) | |
| 145 | + } | |
| 146 | + text := string(script) | |
| 147 | + | |
| 148 | + check := strings.Index(text, "check-version.sh") | |
| 149 | + install := strings.Index(text, "mv -f") | |
| 150 | + switch { | |
| 151 | + case check < 0: | |
| 152 | + t.Fatal("the installer never checks the version it stamped") | |
| 153 | + case install < 0: | |
| 154 | + t.Fatal("the installer no longer installs by rename; this test is out of date") | |
| 155 | + case check > install: | |
| 156 | + t.Error("the installer checks the version after installing, so a bad build replaces a good one") | |
| 157 | + } | |
| 158 | +} | |
| 159 | + | |
| 160 | +// buildRecipe returns the lines of the Makefile's build target. | |
| 161 | +func buildRecipe(t *testing.T, makefile string) string { | |
| 162 | + t.Helper() | |
| 163 | + | |
| 164 | + start := strings.Index(makefile, "\nbuild:") | |
| 165 | + if start < 0 { | |
| 166 | + t.Fatal("the Makefile has no build target") | |
| 167 | + } | |
| 168 | + rest := makefile[start+1:] | |
| 169 | + end := strings.Index(rest, "\n\n") | |
| 170 | + if end < 0 { | |
| 171 | + end = len(rest) | |
| 172 | + } | |
| 173 | + return rest[:end] | |
| 174 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,174 @@ | |||
| 1 | +package main | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "os" | ||
| 5 | + "os/exec" | ||
| 6 | + "path/filepath" | ||
| 7 | + "strings" | ||
| 8 | + "testing" | ||
| 9 | +) | ||
| 10 | + | ||
| 11 | +// Linker flags are a string, and a wrong one is not an error: `-X` naming a | ||
| 12 | +// symbol that does not exist links happily and stamps nothing, so the binary | ||
| 13 | +// falls back to whatever the Go build system knows and reports a version the | ||
| 14 | +// build never meant. Nothing but running the binary catches that, which is why | ||
| 15 | +// `make build` runs it and why these tests do too. | ||
| 16 | + | ||
| 17 | +// buildStampedWith compiles the editor with the flags `make ldflags` produces | ||
| 18 | +// for a version, and returns the path to the binary. | ||
| 19 | +func buildStampedWith(t *testing.T, version string) string { | ||
| 20 | + t.Helper() | ||
| 21 | + | ||
| 22 | + flags, err := exec.Command("make", "--no-print-directory", "ldflags", "VERSION="+version).Output() | ||
| 23 | + if err != nil { | ||
| 24 | + t.Fatalf("make ldflags VERSION=%s: %v", version, err) | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | ||
| 28 | + build := exec.Command("go", "build", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".") | ||
| 29 | + if out, err := build.CombinedOutput(); err != nil { | ||
| 30 | + t.Fatalf("building with those flags failed: %v\n%s", err, out) | ||
| 31 | + } | ||
| 32 | + return binary | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +// checkVersion runs the build's version check and returns what it said and | ||
| 36 | +// whether it was satisfied. | ||
| 37 | +func checkVersion(t *testing.T, args ...string) (string, bool) { | ||
| 38 | + t.Helper() | ||
| 39 | + | ||
| 40 | + out, err := exec.Command("./scripts/check-version.sh", args...).CombinedOutput() | ||
| 41 | + return string(out), err == nil | ||
| 42 | +} | ||
| 43 | + | ||
| 44 | +func TestTheVersionCheckAcceptsTheVersionTheBuildStamped(t *testing.T) { | ||
| 45 | + binary := buildStampedWith(t, "v9.9.9") | ||
| 46 | + | ||
| 47 | + out, ok := checkVersion(t, binary, "v9.9.9") | ||
| 48 | + if !ok { | ||
| 49 | + t.Fatalf("the check refused a correctly stamped binary:\n%s", out) | ||
| 50 | + } | ||
| 51 | + if !strings.Contains(out, "9.9.9") { | ||
| 52 | + t.Errorf("the check reported %q, want it to name the version", out) | ||
| 53 | + } | ||
| 54 | +} | ||
| 55 | + | ||
| 56 | +func TestTheVersionCheckRefusesAVersionThatMerelyContainsTheRightOne(t *testing.T) { | ||
| 57 | + // The reason this is an equality test and not a grep: "0.2.0" is a | ||
| 58 | + // substring of "10.2.0", so a substring check passes a release that ships | ||
| 59 | + // a binary naming an entirely different version. | ||
| 60 | + binary := buildStampedWith(t, "v10.2.0") | ||
| 61 | + | ||
| 62 | + out, ok := checkVersion(t, binary, "v0.2.0") | ||
| 63 | + if ok { | ||
| 64 | + t.Fatalf("the check accepted 10.2.0 as 0.2.0:\n%s", out) | ||
| 65 | + } | ||
| 66 | + if !strings.Contains(out, "10.2.0") { | ||
| 67 | + t.Errorf("the failure does not say what the binary actually reports:\n%s", out) | ||
| 68 | + } | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +func TestTheVersionCheckRefusesAStampThatNeverReachedTheLinker(t *testing.T) { | ||
| 72 | + // The failure this exists for. `-X` naming a symbol that is not there is | ||
| 73 | + // not an error: the binary links, runs, and reports the wrong thing. | ||
| 74 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | ||
| 75 | + build := exec.Command("go", "build", | ||
| 76 | + "-ldflags", "-X 'rickub.com/turbo-editors/turbo-core/version.stampX=v9.9.9'", | ||
| 77 | + "-o", binary, ".") | ||
| 78 | + if out, err := build.CombinedOutput(); err != nil { | ||
| 79 | + t.Fatalf("the build with a misspelt -X failed, so there is nothing to catch: %v\n%s", err, out) | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + out, ok := checkVersion(t, binary, "v9.9.9") | ||
| 83 | + if ok { | ||
| 84 | + t.Fatalf("the check accepted a binary nothing was stamped into:\n%s", out) | ||
| 85 | + } | ||
| 86 | +} | ||
| 87 | + | ||
| 88 | +func TestTheVersionCheckRefusesABinaryThatDoesNotRun(t *testing.T) { | ||
| 89 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | ||
| 90 | + if err := os.WriteFile(binary, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil { | ||
| 91 | + t.Fatalf("cannot write the stand-in: %v", err) | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + if out, ok := checkVersion(t, binary, "v9.9.9"); ok { | ||
| 95 | + t.Fatalf("the check accepted a binary that does not run:\n%s", out) | ||
| 96 | + } | ||
| 97 | +} | ||
| 98 | + | ||
| 99 | +func TestTheVersionCheckNeedsSomethingToCheck(t *testing.T) { | ||
| 100 | + if out, ok := checkVersion(t); ok { | ||
| 101 | + t.Fatalf("the check accepted no arguments at all:\n%s", out) | ||
| 102 | + } | ||
| 103 | + if out, ok := checkVersion(t, filepath.Join(t.TempDir(), "not-there")); ok { | ||
| 104 | + t.Fatalf("the check accepted a path with no binary at it:\n%s", out) | ||
| 105 | + } | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +func TestAnUnstampedBuildIsAcceptedButAnUnnameableOneIsNot(t *testing.T) { | ||
| 109 | + // Installing from a tarball has no git checkout to describe, so there is no | ||
| 110 | + // version to expect. The claim left is that *some* source named it. | ||
| 111 | + binary := filepath.Join(t.TempDir(), "turbo-golo") | ||
| 112 | + build := exec.Command("go", "build", "-o", binary, ".") | ||
| 113 | + if out, err := build.CombinedOutput(); err != nil { | ||
| 114 | + t.Fatalf("an unstamped build failed: %v\n%s", err, out) | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + out, ok := checkVersion(t, binary) | ||
| 118 | + if !ok { | ||
| 119 | + t.Fatalf("the check refused an unstamped build, which is a legitimate one:\n%s", out) | ||
| 120 | + } | ||
| 121 | + if strings.Contains(out, "unknown") { | ||
| 122 | + t.Errorf("the binary cannot name its version and the check passed anyway:\n%s", out) | ||
| 123 | + } | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +func TestTheBuildTargetChecksWhatItStamped(t *testing.T) { | ||
| 127 | + // The wiring, not the script: a check nothing calls protects nothing. | ||
| 128 | + makefile, err := os.ReadFile("Makefile") | ||
| 129 | + if err != nil { | ||
| 130 | + t.Fatalf("cannot read the Makefile: %v", err) | ||
| 131 | + } | ||
| 132 | + recipe := buildRecipe(t, string(makefile)) | ||
| 133 | + | ||
| 134 | + if !strings.Contains(recipe, "check-version.sh") { | ||
| 135 | + t.Errorf("the build target never checks the version it stamped:\n%s", recipe) | ||
| 136 | + } | ||
| 137 | +} | ||
| 138 | + | ||
| 139 | +func TestTheInstallerChecksWhatItStampedBeforeItInstalls(t *testing.T) { | ||
| 140 | + // Before, not after: a binary that cannot name its own version must never | ||
| 141 | + // replace one that can. | ||
| 142 | + script, err := os.ReadFile("scripts/install.sh") | ||
| 143 | + if err != nil { | ||
| 144 | + t.Fatalf("cannot read the installer: %v", err) | ||
| 145 | + } | ||
| 146 | + text := string(script) | ||
| 147 | + | ||
| 148 | + check := strings.Index(text, "check-version.sh") | ||
| 149 | + install := strings.Index(text, "mv -f") | ||
| 150 | + switch { | ||
| 151 | + case check < 0: | ||
| 152 | + t.Fatal("the installer never checks the version it stamped") | ||
| 153 | + case install < 0: | ||
| 154 | + t.Fatal("the installer no longer installs by rename; this test is out of date") | ||
| 155 | + case check > install: | ||
| 156 | + t.Error("the installer checks the version after installing, so a bad build replaces a good one") | ||
| 157 | + } | ||
| 158 | +} | ||
| 159 | + | ||
| 160 | +// buildRecipe returns the lines of the Makefile's build target. | ||
| 161 | +func buildRecipe(t *testing.T, makefile string) string { | ||
| 162 | + t.Helper() | ||
| 163 | + | ||
| 164 | + start := strings.Index(makefile, "\nbuild:") | ||
| 165 | + if start < 0 { | ||
| 166 | + t.Fatal("the Makefile has no build target") | ||
| 167 | + } | ||
| 168 | + rest := makefile[start+1:] | ||
| 169 | + end := strings.Index(rest, "\n\n") | ||
| 170 | + if end < 0 { | ||
| 171 | + end = len(rest) | ||
| 172 | + } | ||
| 173 | + return rest[:end] | ||
| 174 | +} | ||