turbo-editors/turbo-pythonpublic Fork 0
v1.0.0
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.

pythonlang.go · 204 lines · 7.8 KBGo Blame HistoryRaw
📦 Turbo Python 6fc62ea k33g 9h ago1// Package pythonlang is everything about Turbo Python that is about *Python*:
2// how the editor names itself, which language server it talks to, what a
3// project's starter files say, and how Python source is coloured.
4//
5// Everything else the editor does lives in turbo-core, which knows nothing
6// about Python. This package is the whole of the difference between Turbo
7// Python and Turbo Rust, which is what makes a fourth editor a matter of
8// writing one of these rather than forking anything.
9//
10// pythonlang.Register() // teach the library to colour Python
11// editor := app.New(screen, name, pythonlang.Profile())
12package pythonlang
13
14import (
15 "os"
16 "path/filepath"
17
18 "rickub.com/turbo-editors/turbo-core/profile"
19 "rickub.com/turbo-editors/turbo-core/syntax"
20)
21
22// Name and Slug are what the editor calls itself. The slug is also its binary,
23// its project directory (as .turbo-python) and the stem of its environment
24// variables (as TURBO_PYTHON_…), so it is not free to change.
25const (
26 Name = "Turbo Python"
27 Slug = "turbo-python"
28)
29
30// Language is the name Python is known by: the value LanguageOf returns for a
31// .py file, and what a snippets file writes in its languages key.
32const Language syntax.Language = "python"
33
34// ServerCommand is the language server Turbo Python talks to, and InstallHint
35// the single command that installs it.
36//
37// python-lsp-server answers seven of the nine questions turbo-core asks —
38// completion, hover, definition, type definition, references and the file's
39// symbols — and publishes diagnostics unasked. It advertises neither
40// implementations nor a project-wide symbol search, so those two items report
41// nothing found; that is documented rather than worked around.
42//
43// **The [all] is not optional.** Installed bare, pylsp starts, completes and
44// jumps, and publishes an *empty* list of diagnostics for a file that does not
45// parse — because the linters that produce them are extras, and without them
46// the server has nothing to say. An editor whose gutter stays blank because
47// the server has no linter looks exactly like one whose gutter is blank
48// because the code is fine, which is why the hint installs them.
49//
50// pipx is named rather than pip because the server is a tool rather than a
51// dependency of the project being edited, and installing it into that
52// project's environment is how it ends up missing from the next one.
53const (
54 ServerCommand = "pylsp"
55 InstallHint = `pipx install "python-lsp-server[all]"`
56)
57
58// Profile returns the editor Turbo Python is.
59//
60// It is a function rather than a variable because Server.Dirs is worked out
61// from the environment, and a variable would freeze whatever VIRTUAL_ENV said
62// when the package was linked — which for a Python tool is the one value most
63// likely to change between two runs in the same shell.
64func Profile() profile.Profile {
65 return profile.Profile{
66 Name: Name,
67 Slug: Slug,
68 Language: "Python",
69 // P is free: the fixed menus take F, E, S, R, C, O, W, N and H, so the
70 // hot key lands on the first letter of the word, which is the reading
71 // that costs nobody a second glance. The menu is named after the
72 // language and not after uv, because it holds whatever the project put
73 // in its tools file — and the first tools file anybody writes outgrows
74 // the language's own toolchain.
75 ToolsMenu: "~P~ython",
76 // pyproject.toml first because it is where a modern project declares
77 // itself, then the two forms a setuptools project used before it
78 // existed. The nearest one going up is the directory the server is
79 // started in.
80 RootMarkers: []string{"pyproject.toml", "setup.py", "setup.cfg"},
81 Server: profile.Server{
82 Command: ServerCommand,
83 // pylsp takes no subcommand, unlike gopls.
84 Args: nil,
85 InstallHint: InstallHint,
86 Dirs: ServerDirs(),
87 },
88 Templates: profile.Templates{
89 Settings: settingsTemplate,
90 Snippets: snippetsTemplate,
91 Tools: toolsTemplate,
92 Agents: agentsTemplate,
93 },
94 }
95}
96
97// Register teaches turbo-core to colour Python.
98//
99// It is called explicitly at start-up rather than from an init function so that
100// "which languages does this editor know?" is answered by reading main, not by
101// working out which packages were imported.
102func Register() {
103 syntax.Register(syntax.Definition{
104 Language: Language,
105 // .pyw is Windows' "run me without a console window"; .pyi is a stub
106 // file, which is Python and nothing else.
107 Extensions: []string{".py", ".pyi", ".pyw"},
108 // A Python script with no extension at all is an ordinary thing to
109 // find in a bin directory, and its first line says what it is.
110 Shebangs: []string{"python", "python3"},
111 Highlight: Highlight,
112 })
113}
114
115// ServerDirs returns the directories pylsp is looked for in after PATH, most
116// specific first.
117//
118// "Completion silently does nothing" is what a user sees when the editor cannot
119// find a server they believe they installed, and Python has more places to
120// install one than most languages: an environment belonging to this project, a
121// tool directory belonging to this user, a pyenv shim, and — on macOS — a
122// per-version directory under the user's Library that is on nobody's PATH by
123// default.
124//
125// Empty entries are skipped by the library, so a machine with no pyenv and no
126// active environment simply contributes nothing here.
127func ServerDirs() []string {
128 dirs := []string{VirtualEnvBinDir(), UserBinDir(), PyenvShimDir()}
129 return append(dirs, FrameworkScriptDirs()...)
130}
131
132// VirtualEnvBinDir returns the bin directory of the virtual environment that is
133// active right now, or "" when none is.
134//
135// It comes first because a server installed into the project's own environment
136// is the most specific answer available, and because it is the one that stops
137// being true when the user deactivates.
138func VirtualEnvBinDir() string {
139 env := os.Getenv("VIRTUAL_ENV")
140 if env == "" {
141 return ""
142 }
143 return filepath.Join(env, "bin")
144}
145
146// UserBinDir returns ~/.local/bin, where pipx, `uv tool install` and `pip
147// install --user` on Linux all put an executable.
148//
149// It is the directory the install hint's command writes into, so it is the one
150// that matters most to somebody who followed the hint and found nothing.
151func UserBinDir() string {
152 home, err := os.UserHomeDir()
153 if err != nil {
154 return ""
155 }
156 return filepath.Join(home, ".local", "bin")
157}
158
159// PyenvShimDir returns pyenv's shim directory: PYENV_ROOT/shims when PYENV_ROOT
160// is set, and ~/.pyenv/shims otherwise.
161//
162// pyenv works by putting shims on PATH, so this only matters on a machine where
163// its shell hook was never installed — which is exactly the machine where the
164// user cannot work out why nothing is found.
165func PyenvShimDir() string {
166 if root := os.Getenv("PYENV_ROOT"); root != "" {
167 return filepath.Join(root, "shims")
168 }
169 home, err := os.UserHomeDir()
170 if err != nil {
171 return ""
172 }
173 return filepath.Join(home, ".pyenv", "shims")
174}
175
176// FrameworkScriptDirs returns every ~/Library/Python/<version>/bin that exists,
177// in the order the directory lists them — by name, which is not version order.
178//
179// That is where `pip install --user` puts an executable on macOS, and it is on
180// nobody's PATH by default — so a Mac user who installed the server the obvious
181// way has it in a directory the shell has never heard of. The version is part
182// of the path and cannot be predicted, so the directory is read rather than
183// guessed; on a system with no such directory this returns nothing, which is
184// what happens on Linux.
185func FrameworkScriptDirs() []string {
186 home, err := os.UserHomeDir()
187 if err != nil {
188 return nil
189 }
190
191 versions, err := os.ReadDir(filepath.Join(home, "Library", "Python"))
192 if err != nil {
193 return nil
194 }
195
196 var dirs []string
197 for _, version := range versions {
198 if !version.IsDir() {
199 continue
200 }
201 dirs = append(dirs, filepath.Join(home, "Library", "Python", version.Name(), "bin"))
202 }
203 return dirs
204}