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.
Files changed (54) hide show
  1. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.github/workflows/release.yml +13 -42
  2. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/Cargo.lock +1 -1
  3. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/Cargo.toml +1 -1
  4. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/EXTENSION_PUBLISHING.md +48 -8
  5. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/PKG-INFO +11 -2
  6. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/README.md +10 -1
  7. pytest_language_server-0.6.0/demo.gif +0 -0
  8. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/demo.tape +39 -10
  9. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/build.sh +19 -5
  10. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/src/main/resources/META-INF/plugin.xml +1 -1
  11. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/.vscodeignore +3 -0
  12. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/package.json +1 -1
  13. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/pyproject.toml +1 -1
  14. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/src/fixtures.rs +462 -0
  15. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/src/main.rs +73 -0
  16. pytest_language_server-0.5.2/demo.gif +0 -0
  17. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.github/dependabot.yml +0 -0
  18. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.github/workflows/ci.yml +0 -0
  19. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.github/workflows/security.yml +0 -0
  20. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.gitignore +0 -0
  21. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/.pre-commit-config.yaml +0 -0
  22. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/AGENTS.md +0 -0
  23. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/CODE_ACTION_TESTING.md +0 -0
  24. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/EXTENSION_SETUP.md +0 -0
  25. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/Formula/pytest-language-server.rb +0 -0
  26. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/LICENSE +0 -0
  27. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/PERFORMANCE_ANALYSIS.md +0 -0
  28. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/RELEASE.md +0 -0
  29. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/SECURITY.md +0 -0
  30. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/bump-version.sh +0 -0
  31. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/deny.toml +0 -0
  32. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/README.md +0 -0
  33. {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
  34. {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
  35. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/intellij-plugin/src/main/resources/META-INF/pluginIcon.png +0 -0
  36. {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
  37. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/LICENSE +0 -0
  38. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/README.md +0 -0
  39. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/icon.png +0 -0
  40. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/package-lock.json +0 -0
  41. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/src/extension.ts +0 -0
  42. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/tsconfig.json +0 -0
  43. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/extensions/vscode-extension/webpack.config.js +0 -0
  44. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/src/lib.rs +0 -0
  45. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/manual_test.py +0 -0
  46. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_parser_api.rs +0 -0
  47. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/conftest.py +0 -0
  48. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/subdir/conftest.py +0 -0
  49. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/subdir/test_hierarchy.py +0 -0
  50. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/subdir/test_override.py +0 -0
  51. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/test_example.py +0 -0
  52. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/test_parent_usage.py +0 -0
  53. {pytest_language_server-0.5.2 → pytest_language_server-0.6.0}/tests/test_project/test_undeclared_example.py +0 -0
  54. {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
- if: startsWith(github.ref, 'refs/tags/')
344
- needs: [build-binaries]
345
+ # Disabled until the extension is ready to be published
346
+ if: false
345
347
  steps:
346
- - uses: actions/checkout@v4
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
- files: pytest-language-server-zed-extension.tar.gz
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 }}
@@ -753,7 +753,7 @@ dependencies = [
753
753
 
754
754
  [[package]]
755
755
  name = "pytest-language-server"
756
- version = "0.5.2"
756
+ version = "0.6.0"
757
757
  dependencies = [
758
758
  "dashmap 6.1.0",
759
759
  "rustpython-parser",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pytest-language-server"
3
- version = "0.5.2"
3
+ version = "0.6.0"
4
4
  edition = "2021"
5
5
  rust-version = "1.83"
6
6
  authors = ["Thiago Bellini Ribeiro <hackedbellini@gmail.com>"]
@@ -89,17 +89,44 @@ cd extensions/intellij-plugin
89
89
 
90
90
  ### 3. Zed Extension Setup
91
91
 
92
- **Note**: Zed currently uses a manual extension publishing process or requires submitting to their repository.
92
+ **Automated Publishing (via GitHub Action):**
93
93
 
94
- **For Manual Distribution:**
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
- **For Official Zed Extension Directory:**
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
- The extension will still be bundled with binaries in your GitHub releases for users to install manually.
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.5.2
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
  ![pytest-language-server demo](demo.gif)
49
49
 
50
- *Showcasing go-to-definition, find-references, hover documentation, and code actions. Demo also vibed into existence.* ✨
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
  ![pytest-language-server demo](demo.gif)
23
23
 
24
- *Showcasing go-to-definition, find-references, hover documentation, and code actions. Demo also vibed into existence.* ✨
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. Go to Definition"' Enter
57
+ Type 'echo "2. Code Completion"' Enter
49
58
  Sleep 1.5s
50
59
 
51
60
  Type "nvim test_example.py" Enter
52
- Sleep 2s
61
+ Sleep 2.5s
53
62
 
54
63
  Hide
55
64
  Type ":set nu" Enter
56
65
  Show
57
66
 
58
- Type "ggf(w"
59
- Type "gd"
60
- Sleep 2.5s
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
- Type "ZQ"
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
@@ -2,7 +2,10 @@
2
2
  # Simple build script for IntelliJ plugin without Gradle
3
3
  set -e
4
4
 
5
- echo "Building pytest-language-server IntelliJ plugin..."
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
- jar cf ../pytest-language-server.jar META-INF/ classes/
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-0.5.1.zip 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-0.5.1.zip"
39
- ls -lh pytest-language-server-0.5.1.zip
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.5.2</version>
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[
@@ -12,3 +12,6 @@ node_modules/**
12
12
  out/**
13
13
  .eslintrc.json
14
14
  tsconfig.json
15
+
16
+ # Include bundled binaries
17
+ !bin/**
@@ -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.2",
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.5.2"
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