pytest-language-server 0.5.2__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.github/workflows/release.yml +13 -42
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/Cargo.lock +1 -1
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/Cargo.toml +1 -1
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/EXTENSION_PUBLISHING.md +48 -8
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/PKG-INFO +11 -2
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/README.md +10 -1
- pytest_language_server-0.6.0/demo.gif +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/demo.tape +39 -10
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/build.sh +19 -5
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/src/main/resources/META-INF/plugin.xml +1 -1
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/.vscodeignore +3 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/package.json +1 -1
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/pyproject.toml +1 -1
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/src/fixtures.rs +462 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/src/main.rs +73 -0
- pytest_language_server-0.5.2/demo.gif +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.github/dependabot.yml +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.github/workflows/ci.yml +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.github/workflows/security.yml +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.gitignore +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.pre-commit-config.yaml +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/AGENTS.md +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/CODE_ACTION_TESTING.md +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/EXTENSION_SETUP.md +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/Formula/pytest-language-server.rb +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/LICENSE +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/PERFORMANCE_ANALYSIS.md +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/RELEASE.md +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/SECURITY.md +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/bump-version.sh +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/deny.toml +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/README.md +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/src/main/java/com/github/bellini666/pytestlsp/PytestLanguageServerListener.kt +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/src/main/java/com/github/bellini666/pytestlsp/PytestLanguageServerService.kt +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/src/main/resources/META-INF/pluginIcon.png +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/src/main/resources/META-INF/pluginIcon.svg.png +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/LICENSE +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/README.md +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/icon.png +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/package-lock.json +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/src/extension.ts +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/tsconfig.json +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/webpack.config.js +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/src/lib.rs +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/manual_test.py +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_parser_api.rs +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/conftest.py +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/subdir/conftest.py +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/subdir/test_hierarchy.py +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/subdir/test_override.py +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/test_example.py +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/test_parent_usage.py +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/test_undeclared_example.py +0 -0
- {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/uv.lock +0 -0
|
@@ -244,6 +244,8 @@ jobs:
|
|
|
244
244
|
sudo apt-get update
|
|
245
245
|
sudo apt-get install -y gcc-aarch64-linux-gnu
|
|
246
246
|
- name: Build binary
|
|
247
|
+
env:
|
|
248
|
+
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
|
247
249
|
run: |
|
|
248
250
|
cargo build --release --target ${{ matrix.target }}
|
|
249
251
|
- name: Rename binary (Unix)
|
|
@@ -284,16 +286,16 @@ jobs:
|
|
|
284
286
|
ls -lh
|
|
285
287
|
- name: Install dependencies
|
|
286
288
|
run: |
|
|
287
|
-
cd vscode-extension
|
|
289
|
+
cd extensions/vscode-extension
|
|
288
290
|
npm install
|
|
289
291
|
npm install -g @vscode/vsce
|
|
290
292
|
- name: Package extension
|
|
291
293
|
run: |
|
|
292
|
-
cd vscode-extension
|
|
294
|
+
cd extensions/vscode-extension
|
|
293
295
|
vsce package
|
|
294
296
|
- name: Publish to VSCode Marketplace
|
|
295
297
|
run: |
|
|
296
|
-
cd vscode-extension
|
|
298
|
+
cd extensions/vscode-extension
|
|
297
299
|
vsce publish -p ${{ secrets.VSCE_TOKEN }}
|
|
298
300
|
|
|
299
301
|
publish-intellij:
|
|
@@ -340,44 +342,13 @@ jobs:
|
|
|
340
342
|
publish-zed:
|
|
341
343
|
name: Publish Zed extension
|
|
342
344
|
runs-on: ubuntu-latest
|
|
343
|
-
|
|
344
|
-
|
|
345
|
+
# Disabled until the extension is ready to be published
|
|
346
|
+
if: false
|
|
345
347
|
steps:
|
|
346
|
-
- uses:
|
|
347
|
-
- name: Install Rust
|
|
348
|
-
uses: dtolnay/rust-toolchain@stable
|
|
349
|
-
with:
|
|
350
|
-
targets: wasm32-wasip1
|
|
351
|
-
- name: Download all binaries
|
|
352
|
-
uses: actions/download-artifact@v6
|
|
353
|
-
with:
|
|
354
|
-
pattern: binary-*
|
|
355
|
-
path: extensions/zed-extension/bin
|
|
356
|
-
- name: Flatten binaries
|
|
357
|
-
run: |
|
|
358
|
-
cd extensions/zed-extension/bin
|
|
359
|
-
find . -type f -name "pytest-language-server*" -exec mv {} . \;
|
|
360
|
-
find . -type d -empty -delete
|
|
361
|
-
chmod +x pytest-language-server-* || true
|
|
362
|
-
ls -lh
|
|
363
|
-
- name: Build Zed extension
|
|
364
|
-
run: |
|
|
365
|
-
cd zed-extension
|
|
366
|
-
cargo build --release --target wasm32-wasip1
|
|
367
|
-
- name: Package extension
|
|
368
|
-
run: |
|
|
369
|
-
cd zed-extension
|
|
370
|
-
mkdir -p dist
|
|
371
|
-
cp target/wasm32-wasip1/release/pytest_language_server.wasm dist/extension.wasm
|
|
372
|
-
cp -r bin dist/
|
|
373
|
-
cp extension.toml dist/
|
|
374
|
-
- name: Publish to Zed Extensions
|
|
375
|
-
run: |
|
|
376
|
-
cd zed-extension
|
|
377
|
-
# Note: Zed extension publishing requires manual submission or API key
|
|
378
|
-
# For now, just upload the packaged extension to GitHub releases
|
|
379
|
-
tar -czf ../pytest-language-server-zed-extension.tar.gz -C dist .
|
|
380
|
-
- name: Upload Zed extension to release
|
|
381
|
-
uses: softprops/action-gh-release@v2
|
|
348
|
+
- uses: huacnlee/zed-extension-action@v1
|
|
382
349
|
with:
|
|
383
|
-
|
|
350
|
+
extension-name: pytest-language-server
|
|
351
|
+
extension-path: extensions/zed-extension
|
|
352
|
+
push-to: bellini666/extensions
|
|
353
|
+
env:
|
|
354
|
+
COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}
|
|
@@ -89,17 +89,44 @@ cd extensions/intellij-plugin
|
|
|
89
89
|
|
|
90
90
|
### 3. Zed Extension Setup
|
|
91
91
|
|
|
92
|
-
**
|
|
92
|
+
**Automated Publishing (via GitHub Action):**
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
The CI automatically packages the Zed extension as `pytest-language-server-zed-extension.tar.gz` and uploads it to GitHub releases.
|
|
94
|
+
Zed extensions are published by creating PRs to the official Zed extensions repository. This is automated using the `huacnlee/zed-extension-action` GitHub Action.
|
|
96
95
|
|
|
97
|
-
**
|
|
98
|
-
1. Fork https://github.com/zed-industries/extensions
|
|
99
|
-
2. Add extension to `extensions/` directory
|
|
100
|
-
3. Submit PR to Zed extensions repo
|
|
96
|
+
**Setup Steps:**
|
|
101
97
|
|
|
102
|
-
|
|
98
|
+
1. **Fork the Zed Extensions Repo:**
|
|
99
|
+
```bash
|
|
100
|
+
# Go to https://github.com/zed-industries/extensions
|
|
101
|
+
# Click "Fork" (fork to your personal account, not an organization)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
2. **Add Your Extension to Your Fork:**
|
|
105
|
+
```bash
|
|
106
|
+
git clone https://github.com/YOUR_USERNAME/extensions
|
|
107
|
+
cd extensions
|
|
108
|
+
git submodule add https://github.com/bellini666/pytest-language-server.git extensions/pytest-language-server
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
3. **Add Entry to extensions.toml:**
|
|
112
|
+
```toml
|
|
113
|
+
[pytest-language-server]
|
|
114
|
+
submodule = "extensions/pytest-language-server"
|
|
115
|
+
path = "extensions/zed-extension"
|
|
116
|
+
version = "0.5.2"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
4. **Create Initial PR:**
|
|
120
|
+
Submit the initial PR to zed-industries/extensions manually.
|
|
121
|
+
|
|
122
|
+
5. **Automated Updates:**
|
|
123
|
+
After the initial setup, the GitHub Action automatically creates PRs to update your extension when you create a new release tag.
|
|
124
|
+
|
|
125
|
+
**How It Works:**
|
|
126
|
+
- When you push a new version tag (e.g., `v0.6.0`), the `publish-zed` job runs
|
|
127
|
+
- It uses your `COMMITTER_TOKEN` to create a PR on your fork of zed-industries/extensions
|
|
128
|
+
- The PR updates the submodule commit and version in `extensions.toml`
|
|
129
|
+
- You review and merge the PR to your fork, then submit to zed-industries/extensions
|
|
103
130
|
|
|
104
131
|
### 4. GitHub Secrets Configuration
|
|
105
132
|
|
|
@@ -109,8 +136,17 @@ Add these secrets to your GitHub repository (Settings → Secrets and variables
|
|
|
109
136
|
VSCE_TOKEN=<your-vscode-marketplace-token>
|
|
110
137
|
JETBRAINS_TOKEN=<your-jetbrains-marketplace-token>
|
|
111
138
|
CARGO_REGISTRY_TOKEN=<your-crates-io-token>
|
|
139
|
+
COMMITTER_TOKEN=<your-github-pat-with-repo-and-workflow-scopes>
|
|
112
140
|
```
|
|
113
141
|
|
|
142
|
+
**COMMITTER_TOKEN Setup (Required for Zed Extension):**
|
|
143
|
+
1. Go to https://github.com/settings/tokens/new
|
|
144
|
+
2. Create a **Classic** Personal Access Token with these scopes:
|
|
145
|
+
- `repo` (Full control of private repositories)
|
|
146
|
+
- `workflow` (Update GitHub Action workflows)
|
|
147
|
+
3. Copy the token and add it as `COMMITTER_TOKEN` secret
|
|
148
|
+
4. This token allows the Zed extension action to create PRs to your fork of zed-industries/extensions
|
|
149
|
+
|
|
114
150
|
**Optional IntelliJ Plugin Signing (for paid plugins):**
|
|
115
151
|
```
|
|
116
152
|
CERTIFICATE_CHAIN=<your-certificate-chain>
|
|
@@ -277,6 +313,10 @@ If versions get out of sync:
|
|
|
277
313
|
- [ ] Create JetBrains plugin listing
|
|
278
314
|
- [ ] Generate JetBrains token
|
|
279
315
|
- [ ] Add JETBRAINS_TOKEN to GitHub secrets
|
|
316
|
+
- [ ] Fork zed-industries/extensions repository
|
|
317
|
+
- [ ] Generate GitHub PAT with repo & workflow scopes
|
|
318
|
+
- [ ] Add COMMITTER_TOKEN to GitHub secrets
|
|
319
|
+
- [ ] Add CARGO_REGISTRY_TOKEN to GitHub secrets
|
|
280
320
|
- [ ] Test VSCode extension locally
|
|
281
321
|
- [ ] Test IntelliJ plugin locally
|
|
282
322
|
- [ ] Test Zed extension locally
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-language-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -47,7 +47,7 @@ A blazingly fast Language Server Protocol (LSP) implementation for pytest, built
|
|
|
47
47
|
|
|
48
48
|

|
|
49
49
|
|
|
50
|
-
*Showcasing go-to-definition,
|
|
50
|
+
*Showcasing go-to-definition, code completion, hover documentation, and code actions. Demo also vibed into existence.* ✨
|
|
51
51
|
|
|
52
52
|
## Features
|
|
53
53
|
|
|
@@ -58,6 +58,15 @@ Jump directly to fixture definitions from anywhere they're used:
|
|
|
58
58
|
- Third-party fixtures from pytest plugins (pytest-mock, pytest-asyncio, etc.)
|
|
59
59
|
- Respects pytest's fixture shadowing/priority rules
|
|
60
60
|
|
|
61
|
+
### ✨ Code Completion
|
|
62
|
+
Smart auto-completion for pytest fixtures:
|
|
63
|
+
- **Context-aware**: Only triggers inside test functions and fixture functions
|
|
64
|
+
- **Hierarchy-respecting**: Suggests fixtures based on pytest's priority rules (same file > conftest.py > third-party)
|
|
65
|
+
- **Rich information**: Shows fixture source file and docstring
|
|
66
|
+
- **No duplicates**: Automatically filters out shadowed fixtures
|
|
67
|
+
- **Works everywhere**: Completions available in both function parameters and function bodies
|
|
68
|
+
- Supports both sync and async functions
|
|
69
|
+
|
|
61
70
|
### 🔍 Find References
|
|
62
71
|
Find all usages of a fixture across your entire test suite:
|
|
63
72
|
- Works from fixture definitions or usage sites
|
|
@@ -21,7 +21,7 @@ A blazingly fast Language Server Protocol (LSP) implementation for pytest, built
|
|
|
21
21
|
|
|
22
22
|

|
|
23
23
|
|
|
24
|
-
*Showcasing go-to-definition,
|
|
24
|
+
*Showcasing go-to-definition, code completion, hover documentation, and code actions. Demo also vibed into existence.* ✨
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
@@ -32,6 +32,15 @@ Jump directly to fixture definitions from anywhere they're used:
|
|
|
32
32
|
- Third-party fixtures from pytest plugins (pytest-mock, pytest-asyncio, etc.)
|
|
33
33
|
- Respects pytest's fixture shadowing/priority rules
|
|
34
34
|
|
|
35
|
+
### ✨ Code Completion
|
|
36
|
+
Smart auto-completion for pytest fixtures:
|
|
37
|
+
- **Context-aware**: Only triggers inside test functions and fixture functions
|
|
38
|
+
- **Hierarchy-respecting**: Suggests fixtures based on pytest's priority rules (same file > conftest.py > third-party)
|
|
39
|
+
- **Rich information**: Shows fixture source file and docstring
|
|
40
|
+
- **No duplicates**: Automatically filters out shadowed fixtures
|
|
41
|
+
- **Works everywhere**: Completions available in both function parameters and function bodies
|
|
42
|
+
- Supports both sync and async functions
|
|
43
|
+
|
|
35
44
|
### 🔍 Find References
|
|
36
45
|
Find all usages of a fixture across your entire test suite:
|
|
37
46
|
- Works from fixture definitions or usage sites
|
|
Binary file
|
|
@@ -24,9 +24,9 @@ Type "cd /Users/bellini/dev/pytest-language-server/tests/test_project" Enter
|
|
|
24
24
|
Show
|
|
25
25
|
|
|
26
26
|
# Title
|
|
27
|
-
Type 'echo "pytest-language-server Demo"' Enter
|
|
27
|
+
Type 'echo "🐍 pytest-language-server Demo ⚡"' Enter
|
|
28
28
|
Sleep 1.5s
|
|
29
|
-
Type 'echo "1. Hover Documentation"' Enter
|
|
29
|
+
Type 'echo "📖 1. Hover Documentation & Go to Definition"' Enter
|
|
30
30
|
Sleep 1.5s
|
|
31
31
|
|
|
32
32
|
Type "nvim test_example.py" Enter
|
|
@@ -36,34 +36,62 @@ Hide
|
|
|
36
36
|
Type ":set nu" Enter
|
|
37
37
|
Show
|
|
38
38
|
|
|
39
|
+
# Hover on sample_fixture
|
|
39
40
|
Type "ggf(w"
|
|
40
41
|
Sleep 1s
|
|
41
42
|
Type "K"
|
|
42
43
|
Sleep 3.5s
|
|
44
|
+
|
|
45
|
+
# Close hover popup explicitly
|
|
43
46
|
Escape
|
|
47
|
+
Sleep 500ms
|
|
48
|
+
|
|
49
|
+
# Go to definition on same fixture
|
|
50
|
+
Type "gd"
|
|
51
|
+
Sleep 2.5s
|
|
52
|
+
Ctrl+O
|
|
44
53
|
|
|
45
54
|
Type "ZQ"
|
|
46
55
|
Sleep 300ms
|
|
47
56
|
|
|
48
|
-
Type 'echo "2.
|
|
57
|
+
Type 'echo "✨ 2. Code Completion"' Enter
|
|
49
58
|
Sleep 1.5s
|
|
50
59
|
|
|
51
60
|
Type "nvim test_example.py" Enter
|
|
52
|
-
Sleep
|
|
61
|
+
Sleep 2.5s
|
|
53
62
|
|
|
54
63
|
Hide
|
|
55
64
|
Type ":set nu" Enter
|
|
56
65
|
Show
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
Type "
|
|
60
|
-
Sleep
|
|
67
|
+
# Navigate to first test function and go to end of parameters
|
|
68
|
+
Type "gg"
|
|
69
|
+
Sleep 300ms
|
|
70
|
+
Type "f)"
|
|
71
|
+
Sleep 300ms
|
|
72
|
+
Type "i, "
|
|
73
|
+
Sleep 500ms
|
|
74
|
+
|
|
75
|
+
# Trigger completion
|
|
76
|
+
Ctrl+X
|
|
61
77
|
Ctrl+O
|
|
78
|
+
Sleep 2.5s
|
|
62
79
|
|
|
63
|
-
|
|
80
|
+
# Navigate down in completion menu
|
|
81
|
+
Down
|
|
82
|
+
Sleep 500ms
|
|
83
|
+
Down
|
|
84
|
+
Sleep 500ms
|
|
85
|
+
Enter
|
|
86
|
+
Sleep 1.5s
|
|
87
|
+
|
|
88
|
+
# Exit without saving
|
|
89
|
+
Escape
|
|
90
|
+
Sleep 300ms
|
|
91
|
+
Type ":q!" Enter
|
|
64
92
|
Sleep 300ms
|
|
65
93
|
|
|
66
|
-
Type 'echo "3. Code Actions"' Enter
|
|
94
|
+
Type 'echo "🔧 3. Code Actions (Quick Fixes)"' Enter
|
|
67
95
|
Sleep 1.5s
|
|
68
96
|
|
|
69
97
|
Type "nvim test_undeclared_example.py" Enter
|
|
@@ -86,6 +114,7 @@ Type ":9" Enter
|
|
|
86
114
|
Sleep 2s
|
|
87
115
|
|
|
88
116
|
Type "ZQ"
|
|
117
|
+
Sleep 300ms
|
|
89
118
|
|
|
90
|
-
Type 'echo "github.com/bellini666/pytest-language-server"' Enter
|
|
119
|
+
Type 'echo "✨ github.com/bellini666/pytest-language-server ✨"' Enter
|
|
91
120
|
Sleep 2s
|
{pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/build.sh
RENAMED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
# Simple build script for IntelliJ plugin without Gradle
|
|
3
3
|
set -e
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# Extract version from plugin.xml
|
|
6
|
+
VERSION=$(grep -o '<version>[^<]*</version>' src/main/resources/META-INF/plugin.xml | sed 's/<version>\(.*\)<\/version>/\1/')
|
|
7
|
+
|
|
8
|
+
echo "Building pytest-language-server IntelliJ plugin v${VERSION}..."
|
|
6
9
|
|
|
7
10
|
# Clean previous builds
|
|
8
11
|
rm -rf build
|
|
@@ -15,6 +18,13 @@ echo "Copying resources..."
|
|
|
15
18
|
cp src/main/resources/META-INF/plugin.xml build/META-INF/
|
|
16
19
|
cp src/main/resources/META-INF/pluginIcon.png build/META-INF/
|
|
17
20
|
|
|
21
|
+
# Copy bundled binaries if they exist
|
|
22
|
+
if [ -d "src/main/resources/bin" ]; then
|
|
23
|
+
echo "Copying bundled binaries..."
|
|
24
|
+
mkdir -p build/bin
|
|
25
|
+
cp -r src/main/resources/bin/* build/bin/
|
|
26
|
+
fi
|
|
27
|
+
|
|
18
28
|
# Compile Kotlin files (simple compilation without dependencies for now)
|
|
19
29
|
# Note: For a real LSP plugin, you'd need proper IntelliJ SDK compilation
|
|
20
30
|
# For CI, we'll just package the source files which JetBrains can compile
|
|
@@ -24,7 +34,11 @@ cp -r src/main/java/com/github/bellini666/pytestlsp/*.kt build/classes/com/githu
|
|
|
24
34
|
# Create JAR
|
|
25
35
|
echo "Creating plugin JAR..."
|
|
26
36
|
cd build
|
|
27
|
-
|
|
37
|
+
if [ -d "bin" ]; then
|
|
38
|
+
jar cf ../pytest-language-server.jar META-INF/ classes/ bin/
|
|
39
|
+
else
|
|
40
|
+
jar cf ../pytest-language-server.jar META-INF/ classes/
|
|
41
|
+
fi
|
|
28
42
|
|
|
29
43
|
# Create distribution ZIP
|
|
30
44
|
cd ..
|
|
@@ -33,7 +47,7 @@ cp pytest-language-server.jar dist/
|
|
|
33
47
|
cd dist
|
|
34
48
|
mkdir -p pytest-language-server/lib
|
|
35
49
|
mv pytest-language-server.jar pytest-language-server/lib/
|
|
36
|
-
zip -r pytest-language-server
|
|
50
|
+
zip -r pytest-language-server-${VERSION}.zip pytest-language-server/
|
|
37
51
|
|
|
38
|
-
echo "✓ Plugin built successfully: dist/pytest-language-server
|
|
39
|
-
ls -lh pytest-language-server
|
|
52
|
+
echo "✓ Plugin built successfully: dist/pytest-language-server-${VERSION}.zip"
|
|
53
|
+
ls -lh pytest-language-server-${VERSION}.zip
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<idea-plugin>
|
|
2
2
|
<id>com.github.bellini666.pytest-language-server</id>
|
|
3
3
|
<name>pytest Language Server</name>
|
|
4
|
-
<version>0.
|
|
4
|
+
<version>0.6.0</version>
|
|
5
5
|
<vendor email="hackedbellini@gmail.com" url="https://github.com/bellini666/pytest-language-server">Thiago Bellini Ribeiro</vendor>
|
|
6
6
|
|
|
7
7
|
<description><![CDATA[
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "pytest-language-server",
|
|
3
3
|
"displayName": "pytest Language Server",
|
|
4
4
|
"description": "A blazingly fast Language Server Protocol implementation for pytest fixtures",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.6.0",
|
|
6
6
|
"publisher": "bellini666",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "Thiago Bellini Ribeiro <hackedbellini@gmail.com>",
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytest-language-server"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "A blazingly fast Language Server Protocol implementation for pytest"
|
|
9
9
|
authors = [{name = "Thiago Bellini Ribeiro", email = "hackedbellini@gmail.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -1935,6 +1935,174 @@ impl FixtureDatabase {
|
|
|
1935
1935
|
.map(|entry| entry.value().clone())
|
|
1936
1936
|
.unwrap_or_default()
|
|
1937
1937
|
}
|
|
1938
|
+
|
|
1939
|
+
/// Get all available fixtures for a given file, respecting pytest's fixture hierarchy
|
|
1940
|
+
/// Returns a list of fixture definitions sorted by name
|
|
1941
|
+
pub fn get_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
|
|
1942
|
+
let mut available_fixtures = Vec::new();
|
|
1943
|
+
let mut seen_names = std::collections::HashSet::new();
|
|
1944
|
+
|
|
1945
|
+
// Priority 1: Fixtures in the same file
|
|
1946
|
+
for entry in self.definitions.iter() {
|
|
1947
|
+
let fixture_name = entry.key();
|
|
1948
|
+
for def in entry.value().iter() {
|
|
1949
|
+
if def.file_path == file_path && !seen_names.contains(fixture_name.as_str()) {
|
|
1950
|
+
available_fixtures.push(def.clone());
|
|
1951
|
+
seen_names.insert(fixture_name.clone());
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// Priority 2: Fixtures in conftest.py files (walking up the directory tree)
|
|
1957
|
+
if let Some(mut current_dir) = file_path.parent() {
|
|
1958
|
+
loop {
|
|
1959
|
+
let conftest_path = current_dir.join("conftest.py");
|
|
1960
|
+
|
|
1961
|
+
for entry in self.definitions.iter() {
|
|
1962
|
+
let fixture_name = entry.key();
|
|
1963
|
+
for def in entry.value().iter() {
|
|
1964
|
+
if def.file_path == conftest_path
|
|
1965
|
+
&& !seen_names.contains(fixture_name.as_str())
|
|
1966
|
+
{
|
|
1967
|
+
available_fixtures.push(def.clone());
|
|
1968
|
+
seen_names.insert(fixture_name.clone());
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// Move up one directory
|
|
1974
|
+
match current_dir.parent() {
|
|
1975
|
+
Some(parent) => current_dir = parent,
|
|
1976
|
+
None => break,
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// Priority 3: Third-party fixtures from site-packages
|
|
1982
|
+
for entry in self.definitions.iter() {
|
|
1983
|
+
let fixture_name = entry.key();
|
|
1984
|
+
for def in entry.value().iter() {
|
|
1985
|
+
if def.file_path.to_string_lossy().contains("site-packages")
|
|
1986
|
+
&& !seen_names.contains(fixture_name.as_str())
|
|
1987
|
+
{
|
|
1988
|
+
available_fixtures.push(def.clone());
|
|
1989
|
+
seen_names.insert(fixture_name.clone());
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// Sort by name for consistent ordering
|
|
1995
|
+
available_fixtures.sort_by(|a, b| a.name.cmp(&b.name));
|
|
1996
|
+
available_fixtures
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/// Check if a position is inside a test or fixture function (parameter or body)
|
|
2000
|
+
/// Returns Some((function_name, is_fixture, declared_params)) if inside a function
|
|
2001
|
+
pub fn is_inside_function(
|
|
2002
|
+
&self,
|
|
2003
|
+
file_path: &Path,
|
|
2004
|
+
line: u32,
|
|
2005
|
+
character: u32,
|
|
2006
|
+
) -> Option<(String, bool, Vec<String>)> {
|
|
2007
|
+
// Try cache first, then file system
|
|
2008
|
+
let content: Arc<String> = if let Some(cached) = self.file_cache.get(file_path) {
|
|
2009
|
+
Arc::clone(cached.value())
|
|
2010
|
+
} else {
|
|
2011
|
+
Arc::new(std::fs::read_to_string(file_path).ok()?)
|
|
2012
|
+
};
|
|
2013
|
+
|
|
2014
|
+
let target_line = (line + 1) as usize; // Convert to 1-based
|
|
2015
|
+
|
|
2016
|
+
// Parse the file
|
|
2017
|
+
let parsed = parse(&content, Mode::Module, "").ok()?;
|
|
2018
|
+
|
|
2019
|
+
if let rustpython_parser::ast::Mod::Module(module) = parsed {
|
|
2020
|
+
return self.find_enclosing_function(
|
|
2021
|
+
&module.body,
|
|
2022
|
+
&content,
|
|
2023
|
+
target_line,
|
|
2024
|
+
character as usize,
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
None
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
fn find_enclosing_function(
|
|
2032
|
+
&self,
|
|
2033
|
+
stmts: &[Stmt],
|
|
2034
|
+
content: &str,
|
|
2035
|
+
target_line: usize,
|
|
2036
|
+
_target_char: usize,
|
|
2037
|
+
) -> Option<(String, bool, Vec<String>)> {
|
|
2038
|
+
for stmt in stmts {
|
|
2039
|
+
match stmt {
|
|
2040
|
+
Stmt::FunctionDef(func_def) => {
|
|
2041
|
+
let func_start_line = content[..func_def.range.start().to_usize()]
|
|
2042
|
+
.matches('\n')
|
|
2043
|
+
.count()
|
|
2044
|
+
+ 1;
|
|
2045
|
+
let func_end_line = content[..func_def.range.end().to_usize()]
|
|
2046
|
+
.matches('\n')
|
|
2047
|
+
.count()
|
|
2048
|
+
+ 1;
|
|
2049
|
+
|
|
2050
|
+
// Check if target is within this function's range
|
|
2051
|
+
if target_line >= func_start_line && target_line <= func_end_line {
|
|
2052
|
+
let is_fixture = func_def
|
|
2053
|
+
.decorator_list
|
|
2054
|
+
.iter()
|
|
2055
|
+
.any(Self::is_fixture_decorator);
|
|
2056
|
+
let is_test = func_def.name.starts_with("test_");
|
|
2057
|
+
|
|
2058
|
+
// Only return if it's a test or fixture
|
|
2059
|
+
if is_test || is_fixture {
|
|
2060
|
+
let params: Vec<String> = func_def
|
|
2061
|
+
.args
|
|
2062
|
+
.args
|
|
2063
|
+
.iter()
|
|
2064
|
+
.map(|arg| arg.def.arg.to_string())
|
|
2065
|
+
.collect();
|
|
2066
|
+
|
|
2067
|
+
return Some((func_def.name.to_string(), is_fixture, params));
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
Stmt::AsyncFunctionDef(func_def) => {
|
|
2072
|
+
let func_start_line = content[..func_def.range.start().to_usize()]
|
|
2073
|
+
.matches('\n')
|
|
2074
|
+
.count()
|
|
2075
|
+
+ 1;
|
|
2076
|
+
let func_end_line = content[..func_def.range.end().to_usize()]
|
|
2077
|
+
.matches('\n')
|
|
2078
|
+
.count()
|
|
2079
|
+
+ 1;
|
|
2080
|
+
|
|
2081
|
+
if target_line >= func_start_line && target_line <= func_end_line {
|
|
2082
|
+
let is_fixture = func_def
|
|
2083
|
+
.decorator_list
|
|
2084
|
+
.iter()
|
|
2085
|
+
.any(Self::is_fixture_decorator);
|
|
2086
|
+
let is_test = func_def.name.starts_with("test_");
|
|
2087
|
+
|
|
2088
|
+
if is_test || is_fixture {
|
|
2089
|
+
let params: Vec<String> = func_def
|
|
2090
|
+
.args
|
|
2091
|
+
.args
|
|
2092
|
+
.iter()
|
|
2093
|
+
.map(|arg| arg.def.arg.to_string())
|
|
2094
|
+
.collect();
|
|
2095
|
+
|
|
2096
|
+
return Some((func_def.name.to_string(), is_fixture, params));
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
_ => {}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
None
|
|
2105
|
+
}
|
|
1938
2106
|
}
|
|
1939
2107
|
|
|
1940
2108
|
#[cfg(test)]
|
|
@@ -4497,3 +4665,297 @@ def test_mid(fixture_a, fixture_c):
|
|
|
4497
4665
|
"fixture_a from mid-level test should resolve to mid conftest"
|
|
4498
4666
|
);
|
|
4499
4667
|
}
|
|
4668
|
+
|
|
4669
|
+
#[test]
|
|
4670
|
+
fn test_get_available_fixtures_same_file() {
|
|
4671
|
+
let db = FixtureDatabase::new();
|
|
4672
|
+
|
|
4673
|
+
let test_content = r#"
|
|
4674
|
+
import pytest
|
|
4675
|
+
|
|
4676
|
+
@pytest.fixture
|
|
4677
|
+
def fixture_a():
|
|
4678
|
+
return "a"
|
|
4679
|
+
|
|
4680
|
+
@pytest.fixture
|
|
4681
|
+
def fixture_b():
|
|
4682
|
+
return "b"
|
|
4683
|
+
|
|
4684
|
+
def test_something():
|
|
4685
|
+
pass
|
|
4686
|
+
"#;
|
|
4687
|
+
let test_path = PathBuf::from("/tmp/test/test_example.py");
|
|
4688
|
+
db.analyze_file(test_path.clone(), test_content);
|
|
4689
|
+
|
|
4690
|
+
let available = db.get_available_fixtures(&test_path);
|
|
4691
|
+
|
|
4692
|
+
assert_eq!(available.len(), 2, "Should find 2 fixtures in same file");
|
|
4693
|
+
|
|
4694
|
+
let names: Vec<_> = available.iter().map(|f| f.name.as_str()).collect();
|
|
4695
|
+
assert!(names.contains(&"fixture_a"));
|
|
4696
|
+
assert!(names.contains(&"fixture_b"));
|
|
4697
|
+
}
|
|
4698
|
+
|
|
4699
|
+
#[test]
|
|
4700
|
+
fn test_get_available_fixtures_conftest_hierarchy() {
|
|
4701
|
+
let db = FixtureDatabase::new();
|
|
4702
|
+
|
|
4703
|
+
// Root conftest
|
|
4704
|
+
let root_conftest = r#"
|
|
4705
|
+
import pytest
|
|
4706
|
+
|
|
4707
|
+
@pytest.fixture
|
|
4708
|
+
def root_fixture():
|
|
4709
|
+
return "root"
|
|
4710
|
+
"#;
|
|
4711
|
+
let root_path = PathBuf::from("/tmp/test/conftest.py");
|
|
4712
|
+
db.analyze_file(root_path.clone(), root_conftest);
|
|
4713
|
+
|
|
4714
|
+
// Subdir conftest
|
|
4715
|
+
let sub_conftest = r#"
|
|
4716
|
+
import pytest
|
|
4717
|
+
|
|
4718
|
+
@pytest.fixture
|
|
4719
|
+
def sub_fixture():
|
|
4720
|
+
return "sub"
|
|
4721
|
+
"#;
|
|
4722
|
+
let sub_path = PathBuf::from("/tmp/test/subdir/conftest.py");
|
|
4723
|
+
db.analyze_file(sub_path.clone(), sub_conftest);
|
|
4724
|
+
|
|
4725
|
+
// Test file in subdir
|
|
4726
|
+
let test_content = r#"
|
|
4727
|
+
def test_something():
|
|
4728
|
+
pass
|
|
4729
|
+
"#;
|
|
4730
|
+
let test_path = PathBuf::from("/tmp/test/subdir/test_example.py");
|
|
4731
|
+
db.analyze_file(test_path.clone(), test_content);
|
|
4732
|
+
|
|
4733
|
+
let available = db.get_available_fixtures(&test_path);
|
|
4734
|
+
|
|
4735
|
+
assert_eq!(
|
|
4736
|
+
available.len(),
|
|
4737
|
+
2,
|
|
4738
|
+
"Should find fixtures from both conftest files"
|
|
4739
|
+
);
|
|
4740
|
+
|
|
4741
|
+
let names: Vec<_> = available.iter().map(|f| f.name.as_str()).collect();
|
|
4742
|
+
assert!(names.contains(&"root_fixture"));
|
|
4743
|
+
assert!(names.contains(&"sub_fixture"));
|
|
4744
|
+
}
|
|
4745
|
+
|
|
4746
|
+
#[test]
|
|
4747
|
+
fn test_get_available_fixtures_no_duplicates() {
|
|
4748
|
+
let db = FixtureDatabase::new();
|
|
4749
|
+
|
|
4750
|
+
// Root conftest
|
|
4751
|
+
let root_conftest = r#"
|
|
4752
|
+
import pytest
|
|
4753
|
+
|
|
4754
|
+
@pytest.fixture
|
|
4755
|
+
def shared_fixture():
|
|
4756
|
+
return "root"
|
|
4757
|
+
"#;
|
|
4758
|
+
let root_path = PathBuf::from("/tmp/test/conftest.py");
|
|
4759
|
+
db.analyze_file(root_path.clone(), root_conftest);
|
|
4760
|
+
|
|
4761
|
+
// Subdir conftest with same fixture name
|
|
4762
|
+
let sub_conftest = r#"
|
|
4763
|
+
import pytest
|
|
4764
|
+
|
|
4765
|
+
@pytest.fixture
|
|
4766
|
+
def shared_fixture():
|
|
4767
|
+
return "sub"
|
|
4768
|
+
"#;
|
|
4769
|
+
let sub_path = PathBuf::from("/tmp/test/subdir/conftest.py");
|
|
4770
|
+
db.analyze_file(sub_path.clone(), sub_conftest);
|
|
4771
|
+
|
|
4772
|
+
// Test file in subdir
|
|
4773
|
+
let test_content = r#"
|
|
4774
|
+
def test_something():
|
|
4775
|
+
pass
|
|
4776
|
+
"#;
|
|
4777
|
+
let test_path = PathBuf::from("/tmp/test/subdir/test_example.py");
|
|
4778
|
+
db.analyze_file(test_path.clone(), test_content);
|
|
4779
|
+
|
|
4780
|
+
let available = db.get_available_fixtures(&test_path);
|
|
4781
|
+
|
|
4782
|
+
// Should only find one "shared_fixture" (the closest one)
|
|
4783
|
+
let shared_count = available
|
|
4784
|
+
.iter()
|
|
4785
|
+
.filter(|f| f.name == "shared_fixture")
|
|
4786
|
+
.count();
|
|
4787
|
+
assert_eq!(shared_count, 1, "Should only include shared_fixture once");
|
|
4788
|
+
|
|
4789
|
+
// The one included should be from the subdir (closest)
|
|
4790
|
+
let shared_fixture = available
|
|
4791
|
+
.iter()
|
|
4792
|
+
.find(|f| f.name == "shared_fixture")
|
|
4793
|
+
.unwrap();
|
|
4794
|
+
assert_eq!(shared_fixture.file_path, sub_path);
|
|
4795
|
+
}
|
|
4796
|
+
|
|
4797
|
+
#[test]
|
|
4798
|
+
fn test_is_inside_function_in_test() {
|
|
4799
|
+
let db = FixtureDatabase::new();
|
|
4800
|
+
|
|
4801
|
+
let test_content = r#"
|
|
4802
|
+
import pytest
|
|
4803
|
+
|
|
4804
|
+
def test_example(fixture_a, fixture_b):
|
|
4805
|
+
result = fixture_a + fixture_b
|
|
4806
|
+
assert result == "ab"
|
|
4807
|
+
"#;
|
|
4808
|
+
let test_path = PathBuf::from("/tmp/test/test_example.py");
|
|
4809
|
+
db.analyze_file(test_path.clone(), test_content);
|
|
4810
|
+
|
|
4811
|
+
// Test on the function definition line (line 4, 0-indexed line 3)
|
|
4812
|
+
let result = db.is_inside_function(&test_path, 3, 10);
|
|
4813
|
+
assert!(result.is_some());
|
|
4814
|
+
|
|
4815
|
+
let (func_name, is_fixture, params) = result.unwrap();
|
|
4816
|
+
assert_eq!(func_name, "test_example");
|
|
4817
|
+
assert!(!is_fixture);
|
|
4818
|
+
assert_eq!(params, vec!["fixture_a", "fixture_b"]);
|
|
4819
|
+
|
|
4820
|
+
// Test inside the function body (line 5, 0-indexed line 4)
|
|
4821
|
+
let result = db.is_inside_function(&test_path, 4, 10);
|
|
4822
|
+
assert!(result.is_some());
|
|
4823
|
+
|
|
4824
|
+
let (func_name, is_fixture, _) = result.unwrap();
|
|
4825
|
+
assert_eq!(func_name, "test_example");
|
|
4826
|
+
assert!(!is_fixture);
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4829
|
+
#[test]
|
|
4830
|
+
fn test_is_inside_function_in_fixture() {
|
|
4831
|
+
let db = FixtureDatabase::new();
|
|
4832
|
+
|
|
4833
|
+
let test_content = r#"
|
|
4834
|
+
import pytest
|
|
4835
|
+
|
|
4836
|
+
@pytest.fixture
|
|
4837
|
+
def my_fixture(other_fixture):
|
|
4838
|
+
return other_fixture + "_modified"
|
|
4839
|
+
"#;
|
|
4840
|
+
let test_path = PathBuf::from("/tmp/test/conftest.py");
|
|
4841
|
+
db.analyze_file(test_path.clone(), test_content);
|
|
4842
|
+
|
|
4843
|
+
// Test on the function definition line (line 5, 0-indexed line 4)
|
|
4844
|
+
let result = db.is_inside_function(&test_path, 4, 10);
|
|
4845
|
+
assert!(result.is_some());
|
|
4846
|
+
|
|
4847
|
+
let (func_name, is_fixture, params) = result.unwrap();
|
|
4848
|
+
assert_eq!(func_name, "my_fixture");
|
|
4849
|
+
assert!(is_fixture);
|
|
4850
|
+
assert_eq!(params, vec!["other_fixture"]);
|
|
4851
|
+
|
|
4852
|
+
// Test inside the function body (line 6, 0-indexed line 5)
|
|
4853
|
+
let result = db.is_inside_function(&test_path, 5, 10);
|
|
4854
|
+
assert!(result.is_some());
|
|
4855
|
+
|
|
4856
|
+
let (func_name, is_fixture, _) = result.unwrap();
|
|
4857
|
+
assert_eq!(func_name, "my_fixture");
|
|
4858
|
+
assert!(is_fixture);
|
|
4859
|
+
}
|
|
4860
|
+
|
|
4861
|
+
#[test]
|
|
4862
|
+
fn test_is_inside_function_outside() {
|
|
4863
|
+
let db = FixtureDatabase::new();
|
|
4864
|
+
|
|
4865
|
+
let test_content = r#"
|
|
4866
|
+
import pytest
|
|
4867
|
+
|
|
4868
|
+
@pytest.fixture
|
|
4869
|
+
def my_fixture():
|
|
4870
|
+
return "value"
|
|
4871
|
+
|
|
4872
|
+
def test_example(my_fixture):
|
|
4873
|
+
assert my_fixture == "value"
|
|
4874
|
+
|
|
4875
|
+
# This is a comment outside any function
|
|
4876
|
+
"#;
|
|
4877
|
+
let test_path = PathBuf::from("/tmp/test/test_example.py");
|
|
4878
|
+
db.analyze_file(test_path.clone(), test_content);
|
|
4879
|
+
|
|
4880
|
+
// Test on the import line (line 1, 0-indexed line 0)
|
|
4881
|
+
let result = db.is_inside_function(&test_path, 0, 0);
|
|
4882
|
+
assert!(
|
|
4883
|
+
result.is_none(),
|
|
4884
|
+
"Should not be inside a function on import line"
|
|
4885
|
+
);
|
|
4886
|
+
|
|
4887
|
+
// Test on the comment line (line 10, 0-indexed line 9)
|
|
4888
|
+
let result = db.is_inside_function(&test_path, 9, 0);
|
|
4889
|
+
assert!(
|
|
4890
|
+
result.is_none(),
|
|
4891
|
+
"Should not be inside a function on comment line"
|
|
4892
|
+
);
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
#[test]
|
|
4896
|
+
fn test_is_inside_function_non_test() {
|
|
4897
|
+
let db = FixtureDatabase::new();
|
|
4898
|
+
|
|
4899
|
+
let test_content = r#"
|
|
4900
|
+
import pytest
|
|
4901
|
+
|
|
4902
|
+
def helper_function():
|
|
4903
|
+
return "helper"
|
|
4904
|
+
|
|
4905
|
+
def test_example():
|
|
4906
|
+
result = helper_function()
|
|
4907
|
+
assert result == "helper"
|
|
4908
|
+
"#;
|
|
4909
|
+
let test_path = PathBuf::from("/tmp/test/test_example.py");
|
|
4910
|
+
db.analyze_file(test_path.clone(), test_content);
|
|
4911
|
+
|
|
4912
|
+
// Test inside helper_function (not a test or fixture)
|
|
4913
|
+
let result = db.is_inside_function(&test_path, 3, 10);
|
|
4914
|
+
assert!(
|
|
4915
|
+
result.is_none(),
|
|
4916
|
+
"Should not return non-test, non-fixture functions"
|
|
4917
|
+
);
|
|
4918
|
+
|
|
4919
|
+
// Test inside test_example (is a test)
|
|
4920
|
+
let result = db.is_inside_function(&test_path, 6, 10);
|
|
4921
|
+
assert!(result.is_some(), "Should return test functions");
|
|
4922
|
+
|
|
4923
|
+
let (func_name, is_fixture, _) = result.unwrap();
|
|
4924
|
+
assert_eq!(func_name, "test_example");
|
|
4925
|
+
assert!(!is_fixture);
|
|
4926
|
+
}
|
|
4927
|
+
|
|
4928
|
+
#[test]
|
|
4929
|
+
fn test_is_inside_async_function() {
|
|
4930
|
+
let db = FixtureDatabase::new();
|
|
4931
|
+
|
|
4932
|
+
let test_content = r#"
|
|
4933
|
+
import pytest
|
|
4934
|
+
|
|
4935
|
+
@pytest.fixture
|
|
4936
|
+
async def async_fixture():
|
|
4937
|
+
return "async_value"
|
|
4938
|
+
|
|
4939
|
+
async def test_async_example(async_fixture):
|
|
4940
|
+
assert async_fixture == "async_value"
|
|
4941
|
+
"#;
|
|
4942
|
+
let test_path = PathBuf::from("/tmp/test/test_async.py");
|
|
4943
|
+
db.analyze_file(test_path.clone(), test_content);
|
|
4944
|
+
|
|
4945
|
+
// Test inside async fixture (line 5, 0-indexed line 4)
|
|
4946
|
+
let result = db.is_inside_function(&test_path, 4, 10);
|
|
4947
|
+
assert!(result.is_some());
|
|
4948
|
+
|
|
4949
|
+
let (func_name, is_fixture, _) = result.unwrap();
|
|
4950
|
+
assert_eq!(func_name, "async_fixture");
|
|
4951
|
+
assert!(is_fixture);
|
|
4952
|
+
|
|
4953
|
+
// Test inside async test (line 8, 0-indexed line 7)
|
|
4954
|
+
let result = db.is_inside_function(&test_path, 7, 10);
|
|
4955
|
+
assert!(result.is_some());
|
|
4956
|
+
|
|
4957
|
+
let (func_name, is_fixture, params) = result.unwrap();
|
|
4958
|
+
assert_eq!(func_name, "test_async_example");
|
|
4959
|
+
assert!(!is_fixture);
|
|
4960
|
+
assert_eq!(params, vec!["async_fixture"]);
|
|
4961
|
+
}
|
|
@@ -58,6 +58,15 @@ impl LanguageServer for Backend {
|
|
|
58
58
|
resolve_provider: None,
|
|
59
59
|
},
|
|
60
60
|
)),
|
|
61
|
+
completion_provider: Some(CompletionOptions {
|
|
62
|
+
resolve_provider: Some(false),
|
|
63
|
+
trigger_characters: Some(vec![]),
|
|
64
|
+
all_commit_characters: None,
|
|
65
|
+
work_done_progress_options: WorkDoneProgressOptions {
|
|
66
|
+
work_done_progress: None,
|
|
67
|
+
},
|
|
68
|
+
completion_item: None,
|
|
69
|
+
}),
|
|
61
70
|
..Default::default()
|
|
62
71
|
},
|
|
63
72
|
})
|
|
@@ -441,6 +450,70 @@ impl LanguageServer for Backend {
|
|
|
441
450
|
Ok(None)
|
|
442
451
|
}
|
|
443
452
|
|
|
453
|
+
async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
|
|
454
|
+
let uri = params.text_document_position.text_document.uri;
|
|
455
|
+
let position = params.text_document_position.position;
|
|
456
|
+
|
|
457
|
+
info!(
|
|
458
|
+
"completion request: uri={:?}, line={}, char={}",
|
|
459
|
+
uri, position.line, position.character
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if let Ok(file_path) = uri.to_file_path() {
|
|
463
|
+
// Check if we're inside a test or fixture function
|
|
464
|
+
if let Some((function_name, is_fixture, declared_params)) = self
|
|
465
|
+
.fixture_db
|
|
466
|
+
.is_inside_function(&file_path, position.line, position.character)
|
|
467
|
+
{
|
|
468
|
+
info!(
|
|
469
|
+
"Inside function '{}' (is_fixture={}, params={:?})",
|
|
470
|
+
function_name, is_fixture, declared_params
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// Get all available fixtures for this file
|
|
474
|
+
let available_fixtures = self.fixture_db.get_available_fixtures(&file_path);
|
|
475
|
+
info!("Found {} available fixtures", available_fixtures.len());
|
|
476
|
+
|
|
477
|
+
// Convert to completion items
|
|
478
|
+
let completion_items: Vec<CompletionItem> = available_fixtures
|
|
479
|
+
.into_iter()
|
|
480
|
+
.map(|fixture| {
|
|
481
|
+
let mut detail = "pytest fixture".to_string();
|
|
482
|
+
if let Some(file_name) = fixture.file_path.file_name() {
|
|
483
|
+
detail.push_str(&format!(" from {}", file_name.to_string_lossy()));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
let documentation = fixture.docstring.as_ref().map(|doc| {
|
|
487
|
+
Documentation::MarkupContent(MarkupContent {
|
|
488
|
+
kind: MarkupKind::Markdown,
|
|
489
|
+
value: doc.clone(),
|
|
490
|
+
})
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
CompletionItem {
|
|
494
|
+
label: fixture.name.clone(),
|
|
495
|
+
kind: Some(CompletionItemKind::VALUE),
|
|
496
|
+
detail: Some(detail),
|
|
497
|
+
documentation,
|
|
498
|
+
insert_text: Some(fixture.name.clone()),
|
|
499
|
+
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
|
|
500
|
+
..Default::default()
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
.collect();
|
|
504
|
+
|
|
505
|
+
info!("Returning {} completion items", completion_items.len());
|
|
506
|
+
return Ok(Some(CompletionResponse::Array(completion_items)));
|
|
507
|
+
} else {
|
|
508
|
+
info!("Not inside a test or fixture function");
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
warn!("Failed to convert URI to file path: {:?}", uri);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
Ok(None)
|
|
515
|
+
}
|
|
516
|
+
|
|
444
517
|
async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
|
|
445
518
|
let uri = params.text_document.uri;
|
|
446
519
|
let context = params.context;
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
{pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.github/workflows/security.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/Formula/pytest-language-server.rb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/LICENSE
RENAMED
|
File without changes
|
{pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/README.md
RENAMED
|
File without changes
|
{pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/icon.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/conftest.py
RENAMED
|
File without changes
|
{pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/subdir/conftest.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/test_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|