turbo-editors/turbo-pythonpublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-python.git
git clone ssh://git@rickub.com/turbo-editors/turbo-python.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

📦 Turbo Python 6fc62ea · on v1.0.2 · k33g · 8h ago
pythonlang.go · 204 lines · 7.8 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// Package pythonlang is everything about Turbo Python that is about *Python*:
// how the editor names itself, which language server it talks to, what a
// project's starter files say, and how Python source is coloured.
//
// Everything else the editor does lives in turbo-core, which knows nothing
// about Python. This package is the whole of the difference between Turbo
// Python and Turbo Rust, which is what makes a fourth editor a matter of
// writing one of these rather than forking anything.
//
//	pythonlang.Register()               // teach the library to colour Python
//	editor := app.New(screen, name, pythonlang.Profile())
package pythonlang

import (
	"os"
	"path/filepath"

	"rickub.com/turbo-editors/turbo-core/profile"
	"rickub.com/turbo-editors/turbo-core/syntax"
)

// Name and Slug are what the editor calls itself. The slug is also its binary,
// its project directory (as .turbo-python) and the stem of its environment
// variables (as TURBO_PYTHON_…), so it is not free to change.
const (
	Name = "Turbo Python"
	Slug = "turbo-python"
)

// Language is the name Python is known by: the value LanguageOf returns for a
// .py file, and what a snippets file writes in its languages key.
const Language syntax.Language = "python"

// ServerCommand is the language server Turbo Python talks to, and InstallHint
// the single command that installs it.
//
// python-lsp-server answers seven of the nine questions turbo-core asks —
// completion, hover, definition, type definition, references and the file's
// symbols — and publishes diagnostics unasked. It advertises neither
// implementations nor a project-wide symbol search, so those two items report
// nothing found; that is documented rather than worked around.
//
// **The [all] is not optional.** Installed bare, pylsp starts, completes and
// jumps, and publishes an *empty* list of diagnostics for a file that does not
// parse — because the linters that produce them are extras, and without them
// the server has nothing to say. An editor whose gutter stays blank because
// the server has no linter looks exactly like one whose gutter is blank
// because the code is fine, which is why the hint installs them.
//
// pipx is named rather than pip because the server is a tool rather than a
// dependency of the project being edited, and installing it into that
// project's environment is how it ends up missing from the next one.
const (
	ServerCommand = "pylsp"
	InstallHint   = `pipx install "python-lsp-server[all]"`
)

// Profile returns the editor Turbo Python is.
//
// It is a function rather than a variable because Server.Dirs is worked out
// from the environment, and a variable would freeze whatever VIRTUAL_ENV said
// when the package was linked — which for a Python tool is the one value most
// likely to change between two runs in the same shell.
func Profile() profile.Profile {
	return profile.Profile{
		Name:     Name,
		Slug:     Slug,
		Language: "Python",
		// P is free: the fixed menus take F, E, S, R, C, O, W, N and H, so the
		// hot key lands on the first letter of the word, which is the reading
		// that costs nobody a second glance. The menu is named after the
		// language and not after uv, because it holds whatever the project put
		// in its tools file — and the first tools file anybody writes outgrows
		// the language's own toolchain.
		ToolsMenu: "~P~ython",
		// pyproject.toml first because it is where a modern project declares
		// itself, then the two forms a setuptools project used before it
		// existed. The nearest one going up is the directory the server is
		// started in.
		RootMarkers: []string{"pyproject.toml", "setup.py", "setup.cfg"},
		Server: profile.Server{
			Command: ServerCommand,
			// pylsp takes no subcommand, unlike gopls.
			Args:        nil,
			InstallHint: InstallHint,
			Dirs:        ServerDirs(),
		},
		Templates: profile.Templates{
			Settings: settingsTemplate,
			Snippets: snippetsTemplate,
			Tools:    toolsTemplate,
			Agents:   agentsTemplate,
		},
	}
}

// Register teaches turbo-core to colour Python.
//
// It is called explicitly at start-up rather than from an init function so that
// "which languages does this editor know?" is answered by reading main, not by
// working out which packages were imported.
func Register() {
	syntax.Register(syntax.Definition{
		Language: Language,
		// .pyw is Windows' "run me without a console window"; .pyi is a stub
		// file, which is Python and nothing else.
		Extensions: []string{".py", ".pyi", ".pyw"},
		// A Python script with no extension at all is an ordinary thing to
		// find in a bin directory, and its first line says what it is.
		Shebangs:  []string{"python", "python3"},
		Highlight: Highlight,
	})
}

// ServerDirs returns the directories pylsp is looked for in after PATH, most
// specific first.
//
// "Completion silently does nothing" is what a user sees when the editor cannot
// find a server they believe they installed, and Python has more places to
// install one than most languages: an environment belonging to this project, a
// tool directory belonging to this user, a pyenv shim, and — on macOS — a
// per-version directory under the user's Library that is on nobody's PATH by
// default.
//
// Empty entries are skipped by the library, so a machine with no pyenv and no
// active environment simply contributes nothing here.
func ServerDirs() []string {
	dirs := []string{VirtualEnvBinDir(), UserBinDir(), PyenvShimDir()}
	return append(dirs, FrameworkScriptDirs()...)
}

// VirtualEnvBinDir returns the bin directory of the virtual environment that is
// active right now, or "" when none is.
//
// It comes first because a server installed into the project's own environment
// is the most specific answer available, and because it is the one that stops
// being true when the user deactivates.
func VirtualEnvBinDir() string {
	env := os.Getenv("VIRTUAL_ENV")
	if env == "" {
		return ""
	}
	return filepath.Join(env, "bin")
}

// UserBinDir returns ~/.local/bin, where pipx, `uv tool install` and `pip
// install --user` on Linux all put an executable.
//
// It is the directory the install hint's command writes into, so it is the one
// that matters most to somebody who followed the hint and found nothing.
func UserBinDir() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	return filepath.Join(home, ".local", "bin")
}

// PyenvShimDir returns pyenv's shim directory: PYENV_ROOT/shims when PYENV_ROOT
// is set, and ~/.pyenv/shims otherwise.
//
// pyenv works by putting shims on PATH, so this only matters on a machine where
// its shell hook was never installed — which is exactly the machine where the
// user cannot work out why nothing is found.
func PyenvShimDir() string {
	if root := os.Getenv("PYENV_ROOT"); root != "" {
		return filepath.Join(root, "shims")
	}
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	return filepath.Join(home, ".pyenv", "shims")
}

// FrameworkScriptDirs returns every ~/Library/Python/<version>/bin that exists,
// in the order the directory lists them — by name, which is not version order.
//
// That is where `pip install --user` puts an executable on macOS, and it is on
// nobody's PATH by default — so a Mac user who installed the server the obvious
// way has it in a directory the shell has never heard of. The version is part
// of the path and cannot be predicted, so the directory is read rather than
// guessed; on a system with no such directory this returns nothing, which is
// what happens on Linux.
func FrameworkScriptDirs() []string {
	home, err := os.UserHomeDir()
	if err != nil {
		return nil
	}

	versions, err := os.ReadDir(filepath.Join(home, "Library", "Python"))
	if err != nil {
		return nil
	}

	var dirs []string
	for _, version := range versions {
		if !version.IsDir() {
			continue
		}
		dirs = append(dirs, filepath.Join(home, "Library", "Python", version.Name(), "bin"))
	}
	return dirs
}