Skip to content

Commit 54f22ce

Browse files
authored
Add initial version (#1)
1 parent f1f2c2e commit 54f22ce

14 files changed

Lines changed: 341 additions & 0 deletions

.gitattributes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
images/ export-ignore
2+
scripts/ export-ignore
3+
renovate.json export-ignore
4+
tox.ini export-ignore

.github/workflows/main.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: main
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
Lint:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v6
16+
- uses: actions/setup-python@v6
17+
- run: pip3 install tox --user
18+
- run: echo "$HOME/.local/bin" >> $GITHUB_PATH
19+
- run: tox

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
test.py
2+
LSP/
3+
*.sublime-workspace
4+
*.sublime-project

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.8

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 SublimeLSP
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

LSP-pyproject.sublime-commands

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"caption": "Preferences: LSP-pyproject Settings",
4+
"command": "edit_settings",
5+
"args": {
6+
"base_file": "${packages}/LSP-pyproject/LSP-pyproject.sublime-settings",
7+
"default": "// Settings in here override those in \"LSP-pyproject/LSP-pyproject.sublime-settings\"\n{\n\t$0\n}\n"
8+
}
9+
},
10+
]

LSP-pyproject.sublime-settings

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"selector": "source.toml",
3+
"command": [
4+
"${storage_path}/LSP-pyproject/pyproject", "server"
5+
],
6+
}

Main.sublime-menu

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[
2+
{
3+
"id": "preferences",
4+
"children": [
5+
{
6+
"caption": "Package Settings",
7+
"mnemonic": "P",
8+
"id": "package-settings",
9+
"children": [
10+
{
11+
"caption": "LSP",
12+
"id": "lsp-settings",
13+
"children": [
14+
{
15+
"caption": "Servers",
16+
"id": "lsp-servers",
17+
"children": [
18+
{
19+
"caption": "LSP-pyproject",
20+
"command": "edit_settings",
21+
"args": {
22+
"base_file": "${packages}/LSP-pyproject/LSP-pyproject.sublime-settings",
23+
"default": "// Settings in here override those in \"LSP-pyproject/LSP-pyproject.sublime-settings\"\n\n{\n\t$0\n}\n"
24+
}
25+
}
26+
]
27+
}
28+
]
29+
}
30+
]
31+
}
32+
]
33+
}
34+
]

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,29 @@
11
# LSP-pyproject
2+
3+
This is a helper package that starts the [pyproject](https://github.com/terror/pyproject) language server for you.
4+
5+
## Installation
6+
7+
1. Install [LSP](https://packagecontrol.io/packages/LSP) via Package Control.
8+
2. Install [LSP-pyproject](https://packagecontrol.io/packages/LSP-pyproject) via Package Control.
9+
10+
## Applicable files
11+
12+
This language server operates on views with the `source.toml` base scope but only those with a `pyproject.toml` file name.
13+
14+
## Configuration
15+
16+
You can edit the global settings by opening the `Preferences: LSP-pyproject Settings` from the Command Palette.
17+
18+
Configure server-specific rules in your `pyproject.toml` under the `[tool.pyproject]` section.
19+
20+
Each rule can be set to a severity level (`error`, `warning`, `hint`, `information` (or `info`), or `off`) using either a simple string or a table with a `level` field:
21+
22+
```toml
23+
[tool.pyproject.rules]
24+
project-unknown-keys = "warning"
25+
project-dependency-updates = { level = "hint" }
26+
project-requires-python-upper-bound = "off"
27+
```
28+
29+
Rule identifiers are shown in diagnostic output (e.g., `error[project-unknown-keys]`). Rules that aren't explicitly configured use their default severity level.

plugin.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from __future__ import annotations
2+
from http.client import HTTPResponse
3+
from LSP.plugin import AbstractPlugin
4+
from LSP.plugin import ClientConfig
5+
from LSP.plugin import register_plugin
6+
from LSP.plugin import unregister_plugin
7+
from pathlib import Path
8+
from typing import cast, final
9+
from typing_extensions import override
10+
import shutil
11+
import sublime
12+
import tarfile
13+
import urllib.request
14+
import zipfile
15+
16+
17+
TAG = "0.1.2"
18+
ARTIFACT_URL = "https://github.com/terror/pyproject/releases/download/{tag}/{filename}"
19+
ARTIFACT_ARCH_MAPPING = {
20+
'x64': 'x86_64',
21+
'arm64': 'aarch64',
22+
'x32': False,
23+
}
24+
ARTIFACT_PLATFORM_MAPPING = {
25+
'windows': 'pc-windows-msvc',
26+
'osx': 'apple-darwin',
27+
'linux': 'unknown-linux-gnu',
28+
}
29+
30+
31+
def get_artifact_name() -> str:
32+
sublime_arch = sublime.arch()
33+
arch = ARTIFACT_ARCH_MAPPING[sublime_arch]
34+
if arch is False:
35+
raise RuntimeError(f'Unsupported architecture: {sublime_arch}')
36+
sublime_platform = sublime.platform()
37+
platform = ARTIFACT_PLATFORM_MAPPING[sublime_platform]
38+
extension = 'zip' if sublime_platform == 'windows' else 'tar.gz'
39+
return f'pyproject-{TAG}-{arch}-{platform}.{extension}'
40+
41+
42+
@final
43+
class LspPyproject(AbstractPlugin):
44+
45+
@classmethod
46+
@override
47+
def name(cls) -> str:
48+
return 'pyproject'
49+
50+
@classmethod
51+
def basedir(cls) -> Path:
52+
return Path(cls.storage_path()) / str(__package__)
53+
54+
@classmethod
55+
def server_version(cls) -> str:
56+
return TAG
57+
58+
@classmethod
59+
def current_server_version(cls) -> str:
60+
with open(cls.basedir() / "VERSION", "r") as fp:
61+
return fp.read()
62+
63+
@classmethod
64+
@override
65+
def is_applicable(cls, view: sublime.View, config: ClientConfig) -> bool:
66+
is_applicable = super().is_applicable(view, config)
67+
return is_applicable and bool(filename := view.file_name()) and Path(filename).name == 'pyproject.toml'
68+
69+
@classmethod
70+
@override
71+
def needs_update_or_installation(cls) -> bool:
72+
try:
73+
return cls.server_version() != cls.current_server_version()
74+
except OSError:
75+
return True
76+
77+
@classmethod
78+
@override
79+
def install_or_update(cls) -> None:
80+
try:
81+
if cls.basedir().is_dir():
82+
shutil.rmtree(cls.basedir())
83+
cls.basedir().mkdir(exist_ok=True)
84+
version = cls.server_version()
85+
is_windows = sublime.platform() == "windows"
86+
extension = "zip" if is_windows else "tar.gz"
87+
archive_file = cls.basedir() / f"artifact.{extension}"
88+
server_binary_filename = "pyproject.exe" if is_windows else "pyproject"
89+
server_binary_path = cls.basedir() / server_binary_filename
90+
url = ARTIFACT_URL.format(tag=TAG, filename=get_artifact_name())
91+
with urllib.request.urlopen(url) as fp:
92+
with open(archive_file, "wb") as f:
93+
f.write(cast(HTTPResponse, fp).read())
94+
if is_windows:
95+
with zipfile.ZipFile(archive_file, "r") as zip_ref:
96+
zip_ref.extract(server_binary_filename, cls.basedir())
97+
else:
98+
with tarfile.open(archive_file) as fp:
99+
names = fp.getnames()
100+
bad_members = [x for x in names if x.startswith('/') or x.startswith('..')]
101+
if bad_members:
102+
raise Exception(f'{archive_file} appears to be malicious, bad filenames: {bad_members}')
103+
fp.extractall(cls.basedir())
104+
archive_file.unlink()
105+
server_binary_path.chmod(0o744)
106+
with open(cls.basedir() / "VERSION", "w") as fp:
107+
fp.write(version)
108+
except BaseException:
109+
shutil.rmtree(cls.basedir(), ignore_errors=True)
110+
raise
111+
112+
113+
def plugin_loaded() -> None:
114+
register_plugin(LspPyproject)
115+
116+
117+
def plugin_unloaded() -> None:
118+
unregister_plugin(LspPyproject)

0 commit comments

Comments
 (0)