├── logo.png
├── .gitignore
├── .vscode-test.mjs
├── .vscodeignore
├── .vscode
├── extensions.json
├── tasks.json
├── settings.json
└── launch.json
├── src
├── test
│ └── extension.test.ts
├── extension.ts
├── diagnostics.ts
├── linkProvider.ts
├── packageManager.ts
├── commands.ts
└── languageModelTools.ts
├── tsconfig.json
├── eslint.config.mjs
├── LICENSE
├── CHANGELOG.md
├── .github
└── workflows
│ └── main.yml
├── syntaxes
├── toml.tmLanguage.json
└── uvlock.tmLanguage.json
├── README.md
└── package.json
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the0807/UV-Toolkit/HEAD/logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | dist
3 | node_modules
4 | .vscode-test/
5 | *.vsix
6 |
7 | .DS_Store
--------------------------------------------------------------------------------
/.vscode-test.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@vscode/test-cli';
2 |
3 | export default defineConfig({
4 | files: 'out/test/**/*.test.js',
5 | });
6 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | src/**
4 | .gitignore
5 | .yarnrc
6 | vsc-extension-quickstart.md
7 | **/tsconfig.json
8 | **/eslint.config.mjs
9 | **/*.map
10 | **/*.ts
11 | **/.vscode-test.*
12 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dbaeumer.vscode-eslint",
6 | "ms-vscode.extension-test-runner"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "watch",
9 | "problemMatcher": "$tsc-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never"
13 | },
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false // set this to true to hide the "out" folder with the compiled JS files
5 | },
6 | "search.exclude": {
7 | "out": true // set this to false to include "out" folder in search results
8 | },
9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
10 | "typescript.tsc.autoDetect": "off"
11 | }
12 |
--------------------------------------------------------------------------------
/src/test/extension.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert';
2 |
3 | // You can import and use all API from the 'vscode' module
4 | // as well as import your extension to test it
5 | import * as vscode from 'vscode';
6 | // import * as myExtension from '../../extension';
7 |
8 | suite('Extension Test Suite', () => {
9 | vscode.window.showInformationMessage('Start all tests.');
10 |
11 | test('Sample test', () => {
12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5));
13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0));
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "args": [
13 | "--extensionDevelopmentPath=${workspaceFolder}"
14 | ],
15 | "outFiles": [
16 | "${workspaceFolder}/out/**/*.js"
17 | ],
18 | "preLaunchTask": "${defaultBuildTask}"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "Node16",
4 | "target": "ES2022",
5 | "outDir": "out",
6 | "lib": [
7 | "ES2022"
8 | ],
9 | "sourceMap": true,
10 | "rootDir": "src",
11 | "strict": true, /* enable all strict type-checking options */
12 | "noImplicitAny": false, /* Allow implicit any types for flexibility */
13 | /* Additional Checks */
14 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
15 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
16 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import typescriptEslint from "@typescript-eslint/eslint-plugin";
2 | import tsParser from "@typescript-eslint/parser";
3 |
4 | export default [{
5 | files: ["**/*.ts"],
6 | }, {
7 | plugins: {
8 | "@typescript-eslint": typescriptEslint,
9 | },
10 |
11 | languageOptions: {
12 | parser: tsParser,
13 | ecmaVersion: 2022,
14 | sourceType: "module",
15 | },
16 |
17 | rules: {
18 | "@typescript-eslint/naming-convention": ["warn", {
19 | selector: "import",
20 | format: ["camelCase", "PascalCase"],
21 | }],
22 |
23 | curly: "warn",
24 | eqeqeq: "warn",
25 | "no-throw-literal": "warn",
26 | semi: "warn",
27 | },
28 | }];
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { registerLinkProvider } from './linkProvider';
3 | import { registerCommands } from './commands';
4 | import { registerDiagnostics } from './diagnostics';
5 | import { registerPackageManager } from './packageManager';
6 | import { registerLanguageModelTools } from './languageModelTools';
7 |
8 | export function activate(context: vscode.ExtensionContext) {
9 | // Register providers
10 | registerLinkProvider(context);
11 | registerCommands(context);
12 | registerDiagnostics(context);
13 | registerPackageManager(context);
14 |
15 | // Register Language Model Tools for VS Code Copilot integration
16 | registerLanguageModelTools(context);
17 |
18 | // Register language configuration for pyproject.toml
19 | vscode.languages.setLanguageConfiguration('pyproject-toml', {
20 | wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g
21 | });
22 | }
23 |
24 | export function deactivate() {}
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Eom, Taehyun
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.
22 |
--------------------------------------------------------------------------------
/src/diagnostics.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as fs from 'fs';
3 |
4 | export function registerDiagnostics(context: vscode.ExtensionContext) {
5 | const diagnosticCollection = vscode.languages.createDiagnosticCollection('uv');
6 |
7 | async function refreshDiagnostics(document: vscode.TextDocument) {
8 | if (!document.fileName.endsWith('pyproject.toml')) return;
9 |
10 | const pyprojectText = document.getText();
11 | const depMatches = [...pyprojectText.matchAll(/\[dependencies\](.*?)(\n\[|\n*$)/gs)];
12 | const allDeps = depMatches.flatMap(match => match[1].match(/([a-zA-Z0-9_-]+)\s*=\s*["'][^"']+["']/g) || []);
13 | const depNames = allDeps.map(dep => dep.split('=')[0].trim());
14 |
15 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri);
16 | if (!workspaceFolder) return;
17 |
18 | const lockPath = vscode.Uri.joinPath(workspaceFolder.uri, 'uv.lock');
19 | if (!fs.existsSync(lockPath.fsPath)) return;
20 |
21 | const lockText = fs.readFileSync(lockPath.fsPath, 'utf-8');
22 | const missingDeps = depNames.filter(dep => !lockText.includes(dep));
23 |
24 | const diagnostics = missingDeps.map(dep => {
25 | const index = document.getText().indexOf(dep);
26 | const position = document.positionAt(index);
27 | const range = new vscode.Range(position, position.translate(0, dep.length));
28 | return new vscode.Diagnostic(range, `Dependency "${dep}" missing from uv.lock`, vscode.DiagnosticSeverity.Warning);
29 | });
30 |
31 | diagnosticCollection.set(document.uri, diagnostics);
32 | }
33 |
34 | context.subscriptions.push(
35 | vscode.workspace.onDidSaveTextDocument(refreshDiagnostics)
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to the "uv-toolkit" extension will be documented in this file.
4 |
5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
6 |
7 | ## [0.1.7] - 2025-08-11
8 |
9 | - **Version was bumped incorrectly, updated to 0.1.7 instead**
10 |
11 | ## [0.1.2] - 2025-08-11
12 |
13 | - **Test for workflow execution**
14 |
15 | ## [0.1.1] - 2025-08-11
16 |
17 | ### Added
18 | - **Open VSX**: Extension packaging and publishing now fully compatible with Open VSX Registry
19 | - **Virtual Environment Activation**: Direct command to activate UV-managed virtual environments
20 |
21 | ### Improved
22 | - Updated release pipeline to streamline multi-marketplace publishing
23 |
24 | ## [0.1.0] - 2025-08-09
25 |
26 | ### Added
27 | - **VS Code Copilot Integration**: Full support for VS Code Language Model Tools API
28 | - 13 interactive tools for UV package management through Copilot agent mode
29 | - Natural language interface for UV commands (e.g., "Add pandas to my project")
30 | - Tool references: #uv-init, #uv-sync, #uv-add, #uv-upgrade, #uv-clean, etc.
31 | - Rich confirmation dialogs with Markdown formatting
32 | - Comprehensive error handling for LLM consumption
33 | - Seamless integration with existing UV command implementations
34 |
35 | ### Improved
36 | - Enhanced package.json with Language Model Tools configuration
37 | - Updated extension activation to register tools automatically
38 | - Improved TypeScript architecture with proper tool interfaces
39 | - Enhanced documentation with Copilot usage examples
40 |
41 | ## [0.0.2] - 2025-04-09
42 |
43 | ### Fixed
44 | - Bug fixes and stability improvements
45 |
46 | ## [0.0.1] - 2025-04-09
47 |
48 | ### Added
49 | - Python project initialization (uv.init)
50 | - Dependency synchronization and management (uv.sync)
51 | - Package addition and removal (uv.add, uv.removePackage)
52 | - Package search on PyPI (uv.searchPackage)
53 | - Lock file generation and management (uv.generateLock)
54 | - Dependency upgrades (uv.upgradeDependencies)
55 | - Virtual environment creation and management (uv.manageVirtualEnv)
56 | - Python script execution (uv.runScript)
57 | - Script dependency addition (uv.addScriptDependency)
58 | - Python version installation and pinning (uv.installPython, uv.pinPython)
59 | - Tool installation and execution (uv.installTool, uv.runTool)
60 | - Link provider for pyproject.toml and uv.lock files
61 | - Diagnostics for detecting mismatches between pyproject.toml and uv.lock files
62 |
63 | ### Improved
64 | - Enhanced syntax highlighting for pyproject.toml files
65 | - Syntax highlighting support for uv.lock files
66 | - Easy command access through context menus
67 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | release:
4 | types:
5 | - published
6 | workflow_dispatch:
7 | inputs:
8 | publishMS:
9 | description: "Publish to the Microsoft Marketplace"
10 | type: boolean
11 | required: true
12 | default: "true"
13 | publishOVSX:
14 | description: "Publish to Open VSX"
15 | type: boolean
16 | required: true
17 | default: "true"
18 |
19 | jobs:
20 | package:
21 | name: Package
22 | runs-on: ubuntu-latest
23 | outputs:
24 | packageName: ${{ steps.setup.outputs.packageName }}
25 | tag: ${{ steps.setup-tag.outputs.tag }}
26 | version: ${{ steps.setup-tag.outputs.version }}
27 | steps:
28 | - uses: actions/checkout@v4
29 | - uses: actions/setup-node@v4
30 | with:
31 | node-version: 20
32 | registry-url: https://registry.npmjs.org/
33 |
34 | - name: Install dependencies
35 | run: |
36 | npm ci
37 | npm install --save-dev vsce ovsx
38 |
39 | - name: Setup package path
40 | id: setup
41 | run: echo "packageName=$(node -e "console.log(require('./package.json').name + '-' + require('./package.json').version + '.vsix')")" >> $GITHUB_OUTPUT
42 |
43 | - name: Package
44 | run: npx vsce package --out ${{ steps.setup.outputs.packageName }}
45 |
46 | - uses: actions/upload-artifact@v4
47 | with:
48 | name: ${{ steps.setup.outputs.packageName }}
49 | path: ./${{ steps.setup.outputs.packageName }}
50 | if-no-files-found: error
51 |
52 | - name: Setup tag
53 | id: setup-tag
54 | shell: pwsh
55 | run: |
56 | $version = (Get-Content ./package.json -Raw | ConvertFrom-Json).version
57 | "tag=release/$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
58 | "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
59 |
60 | publishMS:
61 | name: Publish to VS marketplace
62 | runs-on: ubuntu-latest
63 | needs: package
64 | if: github.event.inputs.publishMS == 'true'
65 | steps:
66 | - uses: actions/checkout@v4
67 | - uses: actions/download-artifact@v4
68 | with:
69 | name: ${{ needs.package.outputs.packageName }}
70 | - name: Publish to VS marketplace
71 | run: npx vsce publish --packagePath ./${{ needs.package.outputs.packageName }} -p ${{ secrets.VSCE_PAT }}
72 |
73 | publishOVSX:
74 | name: Publish to Open VSX
75 | runs-on: ubuntu-latest
76 | needs: package
77 | if: github.event.inputs.publishOVSX == 'true'
78 | steps:
79 | - uses: actions/checkout@v4
80 | - uses: actions/download-artifact@v4
81 | with:
82 | name: ${{ needs.package.outputs.packageName }}
83 | - name: Publish to Open VSX
84 | run: npx ovsx publish ./${{ needs.package.outputs.packageName }} -p ${{ secrets.OVSX_PAT }}
85 |
--------------------------------------------------------------------------------
/syntaxes/toml.tmLanguage.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Enhanced TOML",
3 | "scopeName": "source.toml.enhanced",
4 | "fileTypes": ["toml"],
5 | "patterns": [
6 | {
7 | "include": "#comments"
8 | },
9 | {
10 | "include": "#tables"
11 | },
12 | {
13 | "include": "#key_value"
14 | }
15 | ],
16 | "repository": {
17 | "comments": {
18 | "match": "(#).*$\\n?",
19 | "name": "comment.line.number-sign.toml",
20 | "captures": {
21 | "1": {
22 | "name": "punctuation.definition.comment.toml"
23 | }
24 | }
25 | },
26 | "tables": {
27 | "match": "^\\s*(\\[)([^\\]]*)(\\])\\s*",
28 | "name": "meta.tag.table.toml",
29 | "captures": {
30 | "1": {
31 | "name": "punctuation.definition.table.toml"
32 | },
33 | "2": {
34 | "name": "entity.name.tag.table.toml"
35 | },
36 | "3": {
37 | "name": "punctuation.definition.table.toml"
38 | }
39 | }
40 | },
41 | "key_value": {
42 | "begin": "([A-Za-z0-9_-]+)\\s*(=)\\s*",
43 | "beginCaptures": {
44 | "1": {
45 | "name": "variable.other.key.toml"
46 | },
47 | "2": {
48 | "name": "keyword.operator.assignment.toml"
49 | }
50 | },
51 | "end": "(?=$|\\n)",
52 | "patterns": [
53 | {
54 | "include": "#string"
55 | },
56 | {
57 | "include": "#number"
58 | },
59 | {
60 | "include": "#boolean"
61 | },
62 | {
63 | "include": "#array"
64 | },
65 | {
66 | "include": "#inline_table"
67 | },
68 | {
69 | "include": "#date"
70 | }
71 | ]
72 | },
73 | "string": {
74 | "patterns": [
75 | {
76 | "name": "string.quoted.double.toml",
77 | "begin": "\"",
78 | "end": "\"",
79 | "patterns": [
80 | {
81 | "name": "constant.character.escape.toml",
82 | "match": "\\\\[btnfr\"\\\\]|\\\\u[0-9A-Fa-f]{4}|\\\\U[0-9A-Fa-f]{8}"
83 | }
84 | ]
85 | },
86 | {
87 | "name": "string.quoted.single.toml",
88 | "begin": "'",
89 | "end": "'"
90 | },
91 | {
92 | "name": "string.quoted.triple.double.toml",
93 | "begin": "\"\"\"",
94 | "end": "\"\"\""
95 | },
96 | {
97 | "name": "string.quoted.triple.single.toml",
98 | "begin": "'''",
99 | "end": "'''"
100 | }
101 | ]
102 | },
103 | "number": {
104 | "patterns": [
105 | {
106 | "name": "constant.numeric.float.toml",
107 | "match": "[-+]?(?:0|[1-9])(?:[0-9_])*(?:\\.[0-9_]+)?(?:[eE][-+]?[0-9_]+)?"
108 | },
109 | {
110 | "name": "constant.numeric.integer.toml",
111 | "match": "[-+]?(?:0|[1-9])(?:[0-9_])*"
112 | },
113 | {
114 | "name": "constant.numeric.hex.toml",
115 | "match": "0x[0-9A-Fa-f_]+"
116 | },
117 | {
118 | "name": "constant.numeric.oct.toml",
119 | "match": "0o[0-7_]+"
120 | },
121 | {
122 | "name": "constant.numeric.bin.toml",
123 | "match": "0b[01_]+"
124 | }
125 | ]
126 | },
127 | "boolean": {
128 | "name": "constant.language.boolean.toml",
129 | "match": "\\b(?:true|false)\\b"
130 | },
131 | "array": {
132 | "name": "meta.structure.array.toml",
133 | "begin": "\\[",
134 | "beginCaptures": {
135 | "0": {
136 | "name": "punctuation.definition.array.begin.toml"
137 | }
138 | },
139 | "end": "\\]",
140 | "endCaptures": {
141 | "0": {
142 | "name": "punctuation.definition.array.end.toml"
143 | }
144 | },
145 | "patterns": [
146 | {
147 | "include": "#string"
148 | },
149 | {
150 | "include": "#number"
151 | },
152 | {
153 | "include": "#boolean"
154 | },
155 | {
156 | "include": "#array"
157 | },
158 | {
159 | "include": "#inline_table"
160 | },
161 | {
162 | "include": "#date"
163 | }
164 | ]
165 | },
166 | "inline_table": {
167 | "name": "meta.structure.inline-table.toml",
168 | "begin": "\\{",
169 | "beginCaptures": {
170 | "0": {
171 | "name": "punctuation.definition.inline-table.begin.toml"
172 | }
173 | },
174 | "end": "\\}",
175 | "endCaptures": {
176 | "0": {
177 | "name": "punctuation.definition.inline-table.end.toml"
178 | }
179 | },
180 | "patterns": [
181 | {
182 | "match": "([A-Za-z0-9_-]+)\\s*(=)",
183 | "captures": {
184 | "1": {
185 | "name": "variable.other.key.toml"
186 | },
187 | "2": {
188 | "name": "keyword.operator.assignment.toml"
189 | }
190 | }
191 | },
192 | {
193 | "include": "#string"
194 | },
195 | {
196 | "include": "#number"
197 | },
198 | {
199 | "include": "#boolean"
200 | },
201 | {
202 | "include": "#array"
203 | },
204 | {
205 | "include": "#date"
206 | }
207 | ]
208 | },
209 | "date": {
210 | "name": "constant.other.date.toml",
211 | "match": "\\d{4}-\\d{2}-\\d{2}(?:[T ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[-+]\\d{2}:\\d{2})?)?"
212 | }
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/syntaxes/uvlock.tmLanguage.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "UV Lock",
3 | "scopeName": "source.uvlock",
4 | "fileTypes": ["lock"],
5 | "patterns": [
6 | {
7 | "include": "#comments"
8 | },
9 | {
10 | "include": "#tables"
11 | },
12 | {
13 | "include": "#package_name"
14 | },
15 | {
16 | "include": "#version"
17 | },
18 | {
19 | "include": "#key_value"
20 | }
21 | ],
22 | "repository": {
23 | "comments": {
24 | "match": "(#).*$\\n?",
25 | "name": "comment.line.number-sign.uvlock",
26 | "captures": {
27 | "1": {
28 | "name": "punctuation.definition.comment.uvlock"
29 | }
30 | }
31 | },
32 | "tables": {
33 | "match": "^\\s*(\\[)([^\\]]*)(\\])\\s*",
34 | "name": "meta.tag.table.uvlock",
35 | "captures": {
36 | "1": {
37 | "name": "punctuation.definition.table.uvlock"
38 | },
39 | "2": {
40 | "name": "entity.name.tag.table.uvlock"
41 | },
42 | "3": {
43 | "name": "punctuation.definition.table.uvlock"
44 | }
45 | }
46 | },
47 | "package_name": {
48 | "match": "^\\s*(name)\\s*(=)\\s*\"([^\"]+)\"",
49 | "captures": {
50 | "1": {
51 | "name": "variable.other.key.uvlock"
52 | },
53 | "2": {
54 | "name": "keyword.operator.assignment.uvlock"
55 | },
56 | "3": {
57 | "name": "entity.name.class.uvlock"
58 | }
59 | }
60 | },
61 | "version": {
62 | "match": "^\\s*(version)\\s*(=)\\s*\"([^\"]+)\"",
63 | "captures": {
64 | "1": {
65 | "name": "variable.other.key.uvlock"
66 | },
67 | "2": {
68 | "name": "keyword.operator.assignment.uvlock"
69 | },
70 | "3": {
71 | "name": "constant.numeric.version.uvlock"
72 | }
73 | }
74 | },
75 | "key_value": {
76 | "begin": "([A-Za-z0-9_-]+)\\s*(=)\\s*",
77 | "beginCaptures": {
78 | "1": {
79 | "name": "variable.other.key.uvlock"
80 | },
81 | "2": {
82 | "name": "keyword.operator.assignment.uvlock"
83 | }
84 | },
85 | "end": "(?=$|\\n)",
86 | "patterns": [
87 | {
88 | "include": "#string"
89 | },
90 | {
91 | "include": "#number"
92 | },
93 | {
94 | "include": "#boolean"
95 | },
96 | {
97 | "include": "#array"
98 | },
99 | {
100 | "include": "#inline_table"
101 | },
102 | {
103 | "include": "#date"
104 | }
105 | ]
106 | },
107 | "string": {
108 | "patterns": [
109 | {
110 | "name": "string.quoted.double.uvlock",
111 | "begin": "\"",
112 | "end": "\"",
113 | "patterns": [
114 | {
115 | "name": "constant.character.escape.uvlock",
116 | "match": "\\\\[btnfr\"\\\\]|\\\\u[0-9A-Fa-f]{4}|\\\\U[0-9A-Fa-f]{8}"
117 | }
118 | ]
119 | },
120 | {
121 | "name": "string.quoted.single.uvlock",
122 | "begin": "'",
123 | "end": "'"
124 | }
125 | ]
126 | },
127 | "number": {
128 | "patterns": [
129 | {
130 | "name": "constant.numeric.float.uvlock",
131 | "match": "[-+]?(?:0|[1-9])(?:[0-9_])*(?:\\.[0-9_]+)?(?:[eE][-+]?[0-9_]+)?"
132 | },
133 | {
134 | "name": "constant.numeric.integer.uvlock",
135 | "match": "[-+]?(?:0|[1-9])(?:[0-9_])*"
136 | }
137 | ]
138 | },
139 | "boolean": {
140 | "name": "constant.language.boolean.uvlock",
141 | "match": "\\b(?:true|false)\\b"
142 | },
143 | "array": {
144 | "name": "meta.structure.array.uvlock",
145 | "begin": "\\[",
146 | "beginCaptures": {
147 | "0": {
148 | "name": "punctuation.definition.array.begin.uvlock"
149 | }
150 | },
151 | "end": "\\]",
152 | "endCaptures": {
153 | "0": {
154 | "name": "punctuation.definition.array.end.uvlock"
155 | }
156 | },
157 | "patterns": [
158 | {
159 | "include": "#string"
160 | },
161 | {
162 | "include": "#number"
163 | },
164 | {
165 | "include": "#boolean"
166 | },
167 | {
168 | "include": "#array"
169 | },
170 | {
171 | "include": "#inline_table"
172 | }
173 | ]
174 | },
175 | "inline_table": {
176 | "name": "meta.structure.inline-table.uvlock",
177 | "begin": "\\{",
178 | "beginCaptures": {
179 | "0": {
180 | "name": "punctuation.definition.inline-table.begin.uvlock"
181 | }
182 | },
183 | "end": "\\}",
184 | "endCaptures": {
185 | "0": {
186 | "name": "punctuation.definition.inline-table.end.uvlock"
187 | }
188 | },
189 | "patterns": [
190 | {
191 | "match": "([A-Za-z0-9_-]+)\\s*(=)",
192 | "captures": {
193 | "1": {
194 | "name": "variable.other.key.uvlock"
195 | },
196 | "2": {
197 | "name": "keyword.operator.assignment.uvlock"
198 | }
199 | }
200 | },
201 | {
202 | "include": "#string"
203 | },
204 | {
205 | "include": "#number"
206 | },
207 | {
208 | "include": "#boolean"
209 | },
210 | {
211 | "include": "#array"
212 | }
213 | ]
214 | },
215 | "date": {
216 | "name": "constant.other.date.uvlock",
217 | "match": "\\d{4}-\\d{2}-\\d{2}(?:[T ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[-+]\\d{2}:\\d{2})?)?"
218 | }
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/src/linkProvider.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 |
3 | export function registerLinkProvider(context: vscode.ExtensionContext) {
4 | // Provider for uv.lock files
5 | const uvLockProvider: vscode.DocumentLinkProvider = {
6 | provideDocumentLinks(document: vscode.TextDocument) {
7 | const links: vscode.DocumentLink[] = [];
8 | for (let line = 0; line < document.lineCount; line++) {
9 | const text = document.lineAt(line).text;
10 | const match = text.match(/^name = "([a-zA-Z0-9-_]+)"/);
11 | if (match) {
12 | const pkgName = match[1];
13 | const start = text.indexOf("name = ") + 8;
14 | const end = start + pkgName.length;
15 | const linkRange = new vscode.Range(line, start, line, end);
16 | const uri = vscode.Uri.parse(`https://pypi.org/project/${pkgName}/`);
17 | links.push(new vscode.DocumentLink(linkRange, uri));
18 | }
19 | }
20 | return links;
21 | }
22 | };
23 |
24 | // Provider for pyproject.toml files
25 | const pyprojectProvider: vscode.DocumentLinkProvider = {
26 | provideDocumentLinks(document: vscode.TextDocument) {
27 | const links: vscode.DocumentLink[] = [];
28 |
29 | // Track sections
30 | let inProjectSection = false;
31 | let inDependenciesSection = false;
32 | let inDependenciesArray = false;
33 |
34 | for (let line = 0; line < document.lineCount; line++) {
35 | const text = document.lineAt(line).text;
36 | const trimmedText = text.trim();
37 |
38 | // Skip empty lines and comments
39 | if (trimmedText === '' || trimmedText.startsWith('#')) {
40 | continue;
41 | }
42 |
43 | // Check if we're entering the project section
44 | if (trimmedText === '[project]') {
45 | inProjectSection = true;
46 | inDependenciesSection = false;
47 | inDependenciesArray = false;
48 | continue;
49 | }
50 |
51 | // Check if we're entering a dependencies section
52 | if (trimmedText === '[dependencies]' || trimmedText === '[dev-dependencies]') {
53 | inProjectSection = false;
54 | inDependenciesSection = true;
55 | inDependenciesArray = false;
56 | continue;
57 | }
58 |
59 | // Check if we're leaving a section
60 | if ((inProjectSection || inDependenciesSection) &&
61 | trimmedText.startsWith('[') && trimmedText.endsWith(']')) {
62 | inProjectSection = false;
63 | inDependenciesSection = false;
64 | inDependenciesArray = false;
65 | continue;
66 | }
67 |
68 | // Check if we're entering dependencies array in project section
69 | if (inProjectSection && trimmedText === 'dependencies = [') {
70 | inDependenciesArray = true;
71 | continue;
72 | }
73 |
74 | // Check if we're leaving dependencies array
75 | if (inDependenciesArray && trimmedText === ']') {
76 | inDependenciesArray = false;
77 | continue;
78 | }
79 |
80 | // Process dependencies in project section array
81 | if (inDependenciesArray) {
82 | // Match package in array format: "package>=version"
83 | const arrayMatch = trimmedText.match(/^\s*"([a-zA-Z0-9_-]+)(?:>=|==|<=|>|<|~=|!=|[^a-zA-Z0-9_-])(.*?)"/);
84 | if (arrayMatch) {
85 | const pkgName = arrayMatch[1];
86 | const start = text.indexOf(pkgName);
87 | const end = start + pkgName.length;
88 | const linkRange = new vscode.Range(line, start, line, end);
89 | const uri = vscode.Uri.parse(`https://pypi.org/project/${pkgName}/`);
90 | links.push(new vscode.DocumentLink(linkRange, uri));
91 | continue;
92 | }
93 | }
94 |
95 | // Process dependencies in traditional section
96 | if (inDependenciesSection) {
97 | // Match package name in key-value format
98 | const match = trimmedText.match(/^([a-zA-Z0-9_-]+)\s*=/);
99 | if (match) {
100 | const pkgName = match[1];
101 | const start = text.indexOf(pkgName);
102 | const end = start + pkgName.length;
103 | const linkRange = new vscode.Range(line, start, line, end);
104 | const uri = vscode.Uri.parse(`https://pypi.org/project/${pkgName}/`);
105 | links.push(new vscode.DocumentLink(linkRange, uri));
106 | }
107 | }
108 | }
109 |
110 | // For debugging
111 | console.log(`Found ${links.length} links in pyproject.toml`);
112 |
113 | return links;
114 | }
115 | };
116 |
117 | // Register for both pyproject-toml and regular toml languages
118 | context.subscriptions.push(
119 | vscode.languages.registerDocumentLinkProvider({ language: 'uvlock' }, uvLockProvider),
120 | vscode.languages.registerDocumentLinkProvider({ language: 'pyproject-toml' }, pyprojectProvider),
121 | vscode.languages.registerDocumentLinkProvider({ language: 'toml', pattern: '**/pyproject.toml' }, pyprojectProvider)
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/src/packageManager.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as fs from 'fs';
3 | import * as path from 'path';
4 |
5 | export function registerPackageManager(context: vscode.ExtensionContext) {
6 | // Register package remove command
7 | const removePackageDisposable = vscode.commands.registerCommand('uv.removePackage', async () => {
8 | // Check if workspace is open
9 | const workspaceFolders = vscode.workspace.workspaceFolders;
10 | if (!workspaceFolders) {
11 | vscode.window.showErrorMessage('No workspace is open.');
12 | return;
13 | }
14 |
15 | // Find pyproject.toml file
16 | const pyprojectPath = path.join(workspaceFolders[0].uri.fsPath, 'pyproject.toml');
17 |
18 | if (!fs.existsSync(pyprojectPath)) {
19 | vscode.window.showErrorMessage('pyproject.toml file not found.');
20 | return;
21 | }
22 |
23 | // Read file content
24 | let content = fs.readFileSync(pyprojectPath, 'utf-8');
25 |
26 | // Extract packages from [dependencies] section
27 | const depMatches = content.match(/\[dependencies\](.*?)(\n\[|\n*$)/s);
28 | if (!depMatches || !depMatches[1]) {
29 | vscode.window.showInformationMessage('No dependencies found in pyproject.toml');
30 | return;
31 | }
32 |
33 | // Parse packages
34 | const depsSection = depMatches[1];
35 | const packageRegex = /([a-zA-Z0-9_-]+)\s*=\s*["']([^"']+)["']/g;
36 | const packages: { name: string, version: string }[] = [];
37 |
38 | let match;
39 | while ((match = packageRegex.exec(depsSection)) !== null) {
40 | packages.push({
41 | name: match[1],
42 | version: match[2]
43 | });
44 | }
45 |
46 | if (packages.length === 0) {
47 | vscode.window.showInformationMessage('No packages found in dependencies.');
48 | return;
49 | }
50 |
51 | // Show quick pick with package names
52 | const selectedPackage = await vscode.window.showQuickPick(
53 | packages.map(pkg => `${pkg.name} (${pkg.version})`),
54 | { placeHolder: 'Select a package to remove' }
55 | );
56 |
57 | if (!selectedPackage) return;
58 |
59 | // Extract package name from selection
60 | const packageName = selectedPackage.split(' ')[0];
61 |
62 | // Remove package from dependencies
63 | const updatedContent = content.replace(
64 | /\[dependencies\](.*?)(\n\[|\n*$)/s,
65 | (match, deps, end) => {
66 | // Remove the package line
67 | const updatedDeps = deps.replace(
68 | new RegExp(`\\s*${packageName}\\s*=\\s*["'][^"']*["']\\s*\\n?`, 'g'),
69 | ''
70 | );
71 | return `[dependencies]${updatedDeps}${end}`;
72 | }
73 | );
74 |
75 | // Save file
76 | fs.writeFileSync(pyprojectPath, updatedContent);
77 | vscode.window.showInformationMessage(`Package ${packageName} removed`);
78 |
79 | // Update editor content if file is open
80 | const openDocuments = vscode.workspace.textDocuments;
81 | const pyprojectDoc = openDocuments.find(doc => doc.fileName === pyprojectPath);
82 | if (pyprojectDoc) {
83 | const edit = new vscode.WorkspaceEdit();
84 | edit.replace(
85 | pyprojectDoc.uri,
86 | new vscode.Range(0, 0, pyprojectDoc.lineCount, 0),
87 | updatedContent
88 | );
89 | await vscode.workspace.applyEdit(edit);
90 | }
91 | });
92 |
93 | context.subscriptions.push(removePackageDisposable);
94 |
95 | // Register package search command
96 | const searchPackageDisposable = vscode.commands.registerCommand('uv.searchPackage', async () => {
97 | // Check if workspace is open
98 | const workspaceFolders = vscode.workspace.workspaceFolders;
99 | if (!workspaceFolders) {
100 | vscode.window.showErrorMessage('No workspace is open.');
101 | return;
102 | }
103 |
104 | // Get search query
105 | const searchQuery = await vscode.window.showInputBox({
106 | placeHolder: 'Package name',
107 | prompt: 'Enter package name to search on PyPI'
108 | });
109 |
110 | if (!searchQuery) return;
111 |
112 | // Show progress while searching
113 | vscode.window.withProgress({
114 | location: vscode.ProgressLocation.Notification,
115 | title: `Searching for "${searchQuery}" on PyPI`,
116 | cancellable: false
117 | }, async (progress) => {
118 | try {
119 | // Use node-fetch or similar to search PyPI
120 | // This is a simplified example - in a real extension, you would use proper API calls
121 | const response = await fetch(`https://pypi.org/pypi/${searchQuery}/json`);
122 |
123 | if (response.ok) {
124 | const data = await response.json() as any;
125 | const latestVersion = data.info.version;
126 | const description = data.info.summary;
127 |
128 | // Show package info and ask to add
129 | const action = await vscode.window.showInformationMessage(
130 | `${searchQuery} (${latestVersion}): ${description}`,
131 | 'Add to dependencies',
132 | 'Cancel'
133 | );
134 |
135 | if (action === 'Add to dependencies') {
136 | // Execute the add package command with pre-filled values
137 | vscode.commands.executeCommand('uv.add', {
138 | packageName: searchQuery,
139 | packageVersion: latestVersion
140 | });
141 | }
142 | } else if (response.status === 404) {
143 | vscode.window.showErrorMessage(`Package "${searchQuery}" not found on PyPI`);
144 | } else {
145 | vscode.window.showErrorMessage(`Error searching PyPI: ${response.statusText}`);
146 | }
147 | } catch (error: any) {
148 | vscode.window.showErrorMessage(`Error searching PyPI: ${error.message}`);
149 | }
150 | });
151 | });
152 |
153 | context.subscriptions.push(searchPackageDisposable);
154 | }
155 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🔧 UV Toolkit
2 |
3 | **UV Toolkit** is a Visual Studio Code extension that enhances your experience when working with [uv](https://github.com/astral-sh/uv), a Python package manager.
4 |
5 | [](https://marketplace.visualstudio.com/items?itemName=the0807.uv-toolkit)
6 |
7 | ## 📝 Overview
8 |
9 | UV Toolkit is an essential tool for Python developers using uv. This extension provides various features such as syntax highlighting, package links, command palette integration, and more to make Python project management easier and more efficient.
10 |
11 | ## ⭐ Key Features at a Glance
12 |
13 | * **VS Code Copilot Integration** - Use UV commands through Copilot agent mode with natural language
14 | * **Command Palette Integration** - Access uv commands directly from VS Code
15 | * **Package Management** - Easily add, remove, and search for packages
16 | * **Dependency Diagnostics** - Get warnings for missing dependencies
17 | * **Package Links** - Click on package names to navigate to PyPI pages
18 | * **Syntax Highlighting** - Enhanced colors for pyproject.toml and uv.lock files
19 | * **Context Menu Integration** - Right-click on files to access UV commands
20 |
21 | ---
22 |
23 | ## ✨ Features
24 |
25 | ### 🤖 VS Code Copilot Integration
26 |
27 | UV Toolkit integrates seamlessly with VS Code Copilot's agent mode, allowing you to use natural language to execute UV commands. Once installed, you can interact with UV through Copilot using the following tools:
28 |
29 | | Tool Reference | Description | Example Usage |
30 | |----------------|-------------|---------------|
31 | | `#uv-init` | Initialize a new Python project | "Initialize a new UV project called 'my-app'" |
32 | | `#uv-sync` | Sync project dependencies | "Sync my project dependencies" |
33 | | `#uv-add` | Add a package to the project | "Add the requests package to my project" |
34 | | `#uv-upgrade` | Upgrade packages | "Upgrade all my packages to the latest versions" |
35 | | `#uv-clean` | Clean UV cache | "Clean the UV cache to free up space" |
36 | | `#uv-lock` | Generate lock file | "Generate a lock file for my project" |
37 | | `#uv-venv` | Create virtual environment | "Create a virtual environment with Python 3.11" |
38 | | `#uv-run` | Run Python script | "Run my main.py script" |
39 | | `#uv-script-dep` | Add script dependency | "Add numpy as a dependency to my script.py" |
40 | | `#uv-python-install` | Install Python version | "Install Python 3.12" |
41 | | `#uv-python-pin` | Pin Python version | "Pin Python 3.11 for this project" |
42 | | `#uv-tool-install` | Install Python tool | "Install the ruff linting tool" |
43 | | `#uvx-run` | Run tool with UVX | "Run black formatter on my code" |
44 |
45 | #### Example Conversations with Copilot:
46 |
47 | **User:** "I need to start a new Python project with UV"
48 | **Copilot:** I'll help you initialize a new UV project. *[Uses #uv-init tool]*
49 |
50 | **User:** "Add pandas and numpy to my data science project"
51 | **Copilot:** I'll add those packages to your project. *[Uses #uv-add tool]*
52 |
53 | **User:** "My dependencies are out of sync, can you fix that?"
54 | **Copilot:** I'll sync your project dependencies. *[Uses #uv-sync tool]*
55 |
56 | ### 🎮 Command Palette Integration
57 | Access common uv commands directly from the VS Code Command Palette:
58 |
59 | | Command | Description |
60 | |---------|-------------|
61 | | `UV: Initialize Project` | Initialize a new Python project |
62 | | `UV: Sync Dependencies` | Synchronize dependencies with uv.lock |
63 | | `UV: Upgrade Packages` | Upgrade all packages |
64 | | `UV: Clean Cache` | Clean uv cache |
65 | | `UV: Add Package to Project` | Add a new package to your dependencies |
66 | | `UV: Add Dev Package to Project` | Add a new package to your development dependencies |
67 | | `UV: Remove Package from pyproject.toml` | Remove a package from your dependencies |
68 | | `UV: Search Package on PyPI` | Search for packages on PyPI and add them to your project |
69 | | `UV: Generate Lock File` | Generate a uv.lock file from your pyproject.toml |
70 | | `UV: Upgrade Dependencies` | Upgrade all dependencies or a specific package |
71 | | `UV: Create Virtual Environment` | Create a new virtual environment |
72 | | `UV: Activate Virtual Environment` | Activate an existing virtual environment |
73 | | `UV: Run Python Script` | Run a Python script with uv |
74 | | `UV: Add Script Dependency` | Add inline dependencies to a Python script |
75 | | `UV: Install Python Version` | Install a specific Python version |
76 | | `UV: Pin Python Version` | Pin a specific Python version for the project |
77 | | `UV: Install Tool` | Install a Python tool with uv |
78 | | `UV: Run Tool with UVX` | Run a Python tool in an ephemeral environment |
79 |
80 | ### 📦 Package Management
81 | Easily add, remove, and search for packages with interactive dialogs.
82 |
83 | ### 🔍 Dependency Diagnostics
84 | Get warnings when dependencies in your `pyproject.toml` are missing from `uv.lock`.
85 |
86 | ### 🔗 Package Links
87 | Click on package names in `uv.lock` and `pyproject.toml` files to navigate directly to their PyPI pages. This makes it easy to check package documentation, versions, and other information.
88 |
89 | ### 📄 Syntax Highlighting
90 | Enhanced syntax highlighting for pyproject.toml and uv.lock files with custom colors for better readability:
91 |
92 | * **Section headers** are highlighted in bold blue
93 | * **Keys** are highlighted in light blue
94 | * **Values** are highlighted based on their type (string, number, boolean)
95 | * **Package names** are highlighted in teal and bold
96 | * **Version numbers** are highlighted in yellow
97 | * **Comments** are highlighted in green and italic
98 |
99 | ### 📋 Context Menu Integration
100 | Right-click on pyproject.toml files in the explorer or editor to access UV commands directly from the context menu.
101 |
102 | ---
103 |
104 | ## 📋 Requirements
105 |
106 | > **IMPORTANT**: This extension requires [uv](https://github.com/astral-sh/uv) to be installed on your system. The extension will not function properly without uv installed and available in your PATH.
107 |
108 | * Visual Studio Code 1.70.0 or higher
109 | * [uv](https://github.com/astral-sh/uv) installed and available in your PATH
110 |
111 | ### 💻 Installing uv
112 |
113 | You can install uv using one of the following methods:
114 |
115 |
116 | macOS / Linux
117 |
118 | ```bash
119 | curl -LsSf https://astral.sh/uv/install.sh | sh
120 | ```
121 |
122 |
123 |
124 | Windows
125 |
126 | ```powershell
127 | powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
128 | ```
129 |
130 |
131 |
132 | Using pip
133 |
134 | ```bash
135 | pip install uv
136 | ```
137 |
138 |
139 |
140 | Using pipx
141 |
142 | ```bash
143 | pipx install uv
144 | ```
145 |
146 |
147 |
148 | Updating uv
149 |
150 | If you've already installed uv, you can update to the latest version with:
151 |
152 | ```bash
153 | uv self update
154 | ```
155 |
156 |
157 | For more detailed installation instructions, visit the [official uv installation documentation](https://github.com/astral-sh/uv).
158 |
159 | ---
160 |
161 | ## ⚙️ Extension Settings
162 |
163 | This extension doesn't have any specific settings.
164 |
165 | ---
166 |
167 | ## 📖 Usage
168 |
169 | ### Basic Usage
170 |
171 | 1. Open a Python project that uses uv
172 | 2. Use the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and search for "UV" to see available commands
173 | 3. Click on package names in `uv.lock` and `pyproject.toml` to navigate to PyPI
174 |
175 | ### Command Usage
176 |
177 |
178 | 🚀 Project Management
179 |
180 | * **UV: Initialize Project**: Initialize a new Python project. Enter a project name to run the uv init command and create a basic project structure.
181 |
182 |
183 |
184 | 📦 Package Management
185 |
186 | * **UV: Add Package to Project**: Add a new package to your project. You can enter the package name, version constraint, and extras.
187 | * **UV: Remove Package from pyproject.toml**: Remove a package from your pyproject.toml file. You can select which package to remove.
188 | * **UV: Search Package on PyPI**: Search for packages on PyPI. You can select a package from the search results to add to your project.
189 | * **UV: Upgrade Packages**: Upgrade all packages to their latest versions.
190 |
191 |
192 |
193 | 🔄 Dependency Management
194 |
195 | * **UV: Sync Dependencies**: Synchronize dependencies with uv.lock. You can choose between basic sync, sync from a specific file, or sync specific groups.
196 | * **UV: Generate Lock File**: Generate a uv.lock file from your pyproject.toml. You can choose between basic compile, include all extras, specify extras, or specify groups.
197 | * **UV: Upgrade Dependencies**: Upgrade all dependencies or a specific package. You can select which package to upgrade.
198 |
199 |
200 |
201 | 🧪 Virtual Environment Management
202 |
203 | * **UV: Create Virtual Environment**: Create a new virtual environment. You can choose between creating a basic venv or creating a venv with a specific Python version.
204 |
205 |
206 |
207 | ▶️ Python Script Execution
208 |
209 | * **UV: Run Python Script**: Run a Python script with uv. You can select which script to run and optionally specify a Python version.
210 | * **UV: Add Script Dependency**: Add inline dependencies to a Python script. You can select a script and a package to add.
211 |
212 |
213 |
214 | 🐍 Python Version Management
215 |
216 | * **UV: Install Python Version**: Install a specific Python version. You can enter which Python version to install.
217 | * **UV: Pin Python Version**: Pin a specific Python version for the project. You can enter which Python version to pin.
218 |
219 |
220 |
221 | 🛠️ Tool Management
222 |
223 | * **UV: Install Tool**: Install a Python tool with uv. You can enter the name of the tool to install.
224 | * **UV: Run Tool with UVX**: Run a Python tool in an ephemeral environment. You can enter the name of the tool to run and optionally enter tool arguments.
225 |
226 |
227 |
228 | 🧹 Miscellaneous
229 |
230 | * **UV: Clean Cache**: Clean the uv cache.
231 |
232 |
233 | ---
234 |
235 | ## 💡 Feedback & Contributions
236 |
237 | We welcome feedback and contributions to improve this extension! Feel free to report issues or suggest improvements on our [GitHub repository](https://github.com/the0807/UV-Toolkit).
238 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uv-toolkit",
3 | "displayName": "UV Toolkit",
4 | "description": "Enhance your Python development experience with uv - the fast Python package manager. Provides syntax highlighting, package links, command integration, and more.",
5 | "version": "0.1.7",
6 | "engines": {
7 | "vscode": "^1.70.0"
8 | },
9 | "icon": "logo.png",
10 | "author": {
11 | "name": "Eom, TaeHyun",
12 | "email": "the0807.eom@gmail.com",
13 | "url": "https://github.com/the0807"
14 | },
15 | "repository": {
16 | "url": "https://github.com/the0807/UV-Toolkit",
17 | "type": "github"
18 | },
19 | "publisher": "the0807",
20 | "categories": [
21 | "Programming Languages",
22 | "Data Science",
23 | "Snippets",
24 | "Other"
25 | ],
26 | "activationEvents": [
27 | "onLanguage:pyproject-toml",
28 | "onLanguage:python",
29 | "onLanguage:uvlock",
30 | "onCommand:uv.init",
31 | "onCommand:uv.sync",
32 | "onCommand:uv.upgrade",
33 | "onCommand:uv.cache.clean",
34 | "onCommand:uv.removePackage",
35 | "onCommand:uv.searchPackage",
36 | "onCommand:uv.generateLock",
37 | "onCommand:uv.upgradeDependencies",
38 | "onCommand:uv.manageVirtualEnv",
39 | "onCommand:uv.activateVirtualEnv",
40 | "onCommand:uv.runScript",
41 | "onCommand:uv.addScriptDependency",
42 | "onCommand:uv.installPython",
43 | "onCommand:uv.pinPython",
44 | "onCommand:uv.installTool",
45 | "onCommand:uv.runTool",
46 | "onCommand:uv.add",
47 | "onCommand:uv.addDev"
48 | ],
49 | "main": "./out/extension.js",
50 | "contributes": {
51 | "languages": [
52 | {
53 | "id": "uvlock",
54 | "extensions": [
55 | ".lock"
56 | ],
57 | "filenames": [
58 | "uv.lock"
59 | ],
60 | "aliases": [
61 | "UV Lock",
62 | "uv.lock"
63 | ]
64 | },
65 | {
66 | "id": "pyproject-toml",
67 | "extensions": [
68 | ".toml"
69 | ],
70 | "filenames": [
71 | "pyproject.toml"
72 | ],
73 | "aliases": [
74 | "PyProject TOML",
75 | "pyproject.toml"
76 | ]
77 | }
78 | ],
79 | "grammars": [
80 | {
81 | "language": "pyproject-toml",
82 | "scopeName": "source.toml.enhanced",
83 | "path": "./syntaxes/toml.tmLanguage.json"
84 | },
85 | {
86 | "language": "uvlock",
87 | "scopeName": "source.uvlock",
88 | "path": "./syntaxes/uvlock.tmLanguage.json"
89 | }
90 | ],
91 | "commands": [
92 | {
93 | "command": "uv.init",
94 | "title": "UV: Initialize Project"
95 | },
96 | {
97 | "command": "uv.sync",
98 | "title": "UV: Sync Dependencies"
99 | },
100 | {
101 | "command": "uv.upgrade",
102 | "title": "UV: Upgrade Packages"
103 | },
104 | {
105 | "command": "uv.cache.clean",
106 | "title": "UV: Clean Cache"
107 | },
108 | {
109 | "command": "uv.removePackage",
110 | "title": "UV: Remove Package from pyproject.toml"
111 | },
112 | {
113 | "command": "uv.searchPackage",
114 | "title": "UV: Search Package on PyPI"
115 | },
116 | {
117 | "command": "uv.generateLock",
118 | "title": "UV: Generate Lock File"
119 | },
120 | {
121 | "command": "uv.upgradeDependencies",
122 | "title": "UV: Upgrade Dependencies"
123 | },
124 | {
125 | "command": "uv.manageVirtualEnv",
126 | "title": "UV: Create Virtual Environment"
127 | },
128 | {
129 | "command": "uv.activateVirtualEnv",
130 | "title": "UV: Activate Virtual Environment"
131 | },
132 | {
133 | "command": "uv.runScript",
134 | "title": "UV: Run Python Script"
135 | },
136 | {
137 | "command": "uv.addScriptDependency",
138 | "title": "UV: Add Script Dependency"
139 | },
140 | {
141 | "command": "uv.installPython",
142 | "title": "UV: Install Python Version"
143 | },
144 | {
145 | "command": "uv.pinPython",
146 | "title": "UV: Pin Python Version"
147 | },
148 | {
149 | "command": "uv.installTool",
150 | "title": "UV: Install Tool"
151 | },
152 | {
153 | "command": "uv.runTool",
154 | "title": "UV: Run Tool with UVX"
155 | },
156 | {
157 | "command": "uv.add",
158 | "title": "UV: Add Package to Project"
159 | },
160 | {
161 | "command": "uv.addDev",
162 | "title": "UV: Add Dev Package to Project"
163 | }
164 | ],
165 | "menus": {
166 | "explorer/context": [
167 | {
168 | "when": "resourceFilename == pyproject.toml",
169 | "command": "uv.add",
170 | "group": "uv"
171 | },
172 | {
173 | "when": "resourceFilename == pyproject.toml",
174 | "command": "uv.addDev",
175 | "group": "uv"
176 | },
177 | {
178 | "when": "resourceFilename == pyproject.toml",
179 | "command": "uv.removePackage",
180 | "group": "uv"
181 | },
182 | {
183 | "when": "resourceFilename == pyproject.toml",
184 | "command": "uv.searchPackage",
185 | "group": "uv"
186 | },
187 | {
188 | "when": "resourceFilename == pyproject.toml",
189 | "command": "uv.generateLock",
190 | "group": "uv"
191 | },
192 | {
193 | "when": "resourceFilename == pyproject.toml",
194 | "command": "uv.upgradeDependencies",
195 | "group": "uv"
196 | }
197 | ],
198 | "editor/context": [
199 | {
200 | "when": "resourceFilename == pyproject.toml",
201 | "command": "uv.add",
202 | "group": "uv"
203 | },
204 | {
205 | "when": "resourceFilename == pyproject.toml",
206 | "command": "uv.addDev",
207 | "group": "uv"
208 | },
209 | {
210 | "when": "resourceFilename == pyproject.toml",
211 | "command": "uv.removePackage",
212 | "group": "uv"
213 | },
214 | {
215 | "when": "resourceFilename == pyproject.toml",
216 | "command": "uv.searchPackage",
217 | "group": "uv"
218 | },
219 | {
220 | "when": "resourceFilename == pyproject.toml",
221 | "command": "uv.generateLock",
222 | "group": "uv"
223 | },
224 | {
225 | "when": "resourceFilename == pyproject.toml",
226 | "command": "uv.upgradeDependencies",
227 | "group": "uv"
228 | }
229 | ],
230 | "explorer/context/resource/group": [
231 | {
232 | "when": "resourceExtname == .py",
233 | "command": "uv.runScript",
234 | "group": "uv"
235 | },
236 | {
237 | "when": "resourceExtname == .py",
238 | "command": "uv.addScriptDependency",
239 | "group": "uv"
240 | }
241 | ],
242 | "editor/context/resource": [
243 | {
244 | "when": "resourceExtname == .py",
245 | "command": "uv.runScript",
246 | "group": "uv"
247 | },
248 | {
249 | "when": "resourceExtname == .py",
250 | "command": "uv.addScriptDependency",
251 | "group": "uv"
252 | }
253 | ],
254 | "commandPalette": [
255 | {
256 | "command": "uv.init",
257 | "group": "uv"
258 | },
259 | {
260 | "command": "uv.installPython",
261 | "group": "uv"
262 | },
263 | {
264 | "command": "uv.pinPython",
265 | "group": "uv"
266 | },
267 | {
268 | "command": "uv.installTool",
269 | "group": "uv"
270 | },
271 | {
272 | "command": "uv.runTool",
273 | "group": "uv"
274 | }
275 | ]
276 | },
277 | "configuration": {
278 | "title": "UV Toolkit"
279 | },
280 | "languageModelTools": [
281 | {
282 | "name": "init_project",
283 | "displayName": "Initialize UV Project",
284 | "canBeReferencedInPrompt": true,
285 | "toolReferenceName": "uv-init",
286 | "icon": "$(new-folder)",
287 | "userDescription": "Initialize a new Python project with UV package manager",
288 | "modelDescription": "Initialize a new Python project using UV package manager. This tool creates a new pyproject.toml file and sets up the project structure. Use when the user wants to start a new Python project or when they mention initializing/creating a new UV project. The tool will prompt for a project name if not provided.",
289 | "inputSchema": {
290 | "type": "object",
291 | "properties": {
292 | "projectName": {
293 | "type": "string",
294 | "description": "Name of the project to initialize. If not provided, will use the workspace folder name."
295 | }
296 | }
297 | }
298 | },
299 | {
300 | "name": "sync_dependencies",
301 | "displayName": "Sync Dependencies",
302 | "canBeReferencedInPrompt": true,
303 | "toolReferenceName": "uv-sync",
304 | "icon": "$(sync)",
305 | "userDescription": "Sync project dependencies using UV",
306 | "modelDescription": "Synchronize project dependencies based on pyproject.toml or requirements files. Use when the user wants to install or update dependencies, mentions syncing packages, or needs to ensure their environment matches the project requirements. Supports syncing specific groups or from specific files.",
307 | "inputSchema": {
308 | "type": "object",
309 | "properties": {
310 | "syncType": {
311 | "type": "string",
312 | "enum": [
313 | "basic",
314 | "from-file",
315 | "groups"
316 | ],
317 | "description": "Type of sync operation to perform",
318 | "default": "basic"
319 | },
320 | "requirementsFile": {
321 | "type": "string",
322 | "description": "Path to requirements file (when syncType is 'from-file')"
323 | },
324 | "groups": {
325 | "type": "string",
326 | "description": "Comma-separated list of dependency groups to sync (when syncType is 'groups')"
327 | }
328 | }
329 | }
330 | },
331 | {
332 | "name": "add_package",
333 | "displayName": "Add Package",
334 | "canBeReferencedInPrompt": true,
335 | "toolReferenceName": "uv-add",
336 | "icon": "$(add)",
337 | "userDescription": "Add a Python package to the project",
338 | "modelDescription": "Add a Python package to the project's dependencies in pyproject.toml. Use when the user wants to install a new package, mentions adding a dependency, or needs to include a specific library in their project. Supports version constraints and extras.",
339 | "inputSchema": {
340 | "type": "object",
341 | "properties": {
342 | "packageName": {
343 | "type": "string",
344 | "description": "Name of the package to add (required)"
345 | },
346 | "version": {
347 | "type": "string",
348 | "description": "Version constraint for the package (optional, e.g., '>=1.0.0', '==2.1.0')"
349 | },
350 | "extras": {
351 | "type": "string",
352 | "description": "Comma-separated list of extras to include (optional, e.g., 'dev,test')"
353 | }
354 | },
355 | "required": [
356 | "packageName"
357 | ]
358 | }
359 | },
360 | {
361 | "name": "add_dev_package",
362 | "displayName": "Add Dev Package",
363 | "canBeReferencedInPrompt": true,
364 | "toolReferenceName": "uv-add-dev",
365 | "icon": "$(add)",
366 | "userDescription": "Add a Python package to the project as a development dependency",
367 | "modelDescription": "Add a Python package to the project's development dependencies in pyproject.toml. Use when the user wants to install a development package, mentions adding a dev dependency, or needs to include a library for development/testing purposes. Supports version constraints and extras.",
368 | "inputSchema": {
369 | "type": "object",
370 | "properties": {
371 | "packageName": {
372 | "type": "string",
373 | "description": "Name of the package to add as a development dependency (required)"
374 | },
375 | "version": {
376 | "type": "string",
377 | "description": "Version constraint for the package (optional, e.g., '>=1.0.0', '==2.1.0')"
378 | },
379 | "extras": {
380 | "type": "string",
381 | "description": "Comma-separated list of extras to include (optional, e.g., 'dev,test')"
382 | }
383 | },
384 | "required": [
385 | "packageName"
386 | ]
387 | }
388 | },
389 | {
390 | "name": "upgrade_packages",
391 | "displayName": "Upgrade Packages",
392 | "canBeReferencedInPrompt": true,
393 | "toolReferenceName": "uv-upgrade",
394 | "icon": "$(arrow-up)",
395 | "userDescription": "Upgrade Python packages to their latest versions",
396 | "modelDescription": "Upgrade project dependencies to their latest compatible versions. Use when the user wants to update packages, mentions upgrading dependencies, or needs to get the latest versions of their libraries. Can upgrade all packages or specific ones.",
397 | "inputSchema": {
398 | "type": "object",
399 | "properties": {
400 | "upgradeType": {
401 | "type": "string",
402 | "enum": [
403 | "all",
404 | "specific"
405 | ],
406 | "description": "Whether to upgrade all packages or a specific package",
407 | "default": "all"
408 | },
409 | "packageName": {
410 | "type": "string",
411 | "description": "Name of specific package to upgrade (when upgradeType is 'specific')"
412 | }
413 | }
414 | }
415 | },
416 | {
417 | "name": "clean_cache",
418 | "displayName": "Clean Cache",
419 | "canBeReferencedInPrompt": true,
420 | "toolReferenceName": "uv-clean",
421 | "icon": "$(trash)",
422 | "userDescription": "Clean UV package cache",
423 | "modelDescription": "Clean the UV package cache to free up disk space and resolve caching issues. Use when the user mentions cache problems, wants to clear storage, or needs to resolve package installation issues that might be cache-related.",
424 | "inputSchema": {
425 | "type": "object",
426 | "properties": {}
427 | }
428 | },
429 | {
430 | "name": "generate_lock",
431 | "displayName": "Generate Lock File",
432 | "canBeReferencedInPrompt": true,
433 | "toolReferenceName": "uv-lock",
434 | "icon": "$(lock)",
435 | "userDescription": "Generate uv.lock file from pyproject.toml",
436 | "modelDescription": "Generate a lock file (uv.lock) from pyproject.toml to pin exact dependency versions. Use when the user wants to create reproducible builds, mentions lock files, or needs to freeze dependency versions. Supports including extras and specific groups.",
437 | "inputSchema": {
438 | "type": "object",
439 | "properties": {
440 | "includeExtras": {
441 | "type": "string",
442 | "enum": [
443 | "none",
444 | "all",
445 | "specific"
446 | ],
447 | "description": "Which extras to include in the lock file",
448 | "default": "none"
449 | },
450 | "extras": {
451 | "type": "string",
452 | "description": "Comma-separated list of specific extras to include (when includeExtras is 'specific')"
453 | },
454 | "groups": {
455 | "type": "string",
456 | "description": "Comma-separated list of dependency groups to include"
457 | }
458 | }
459 | }
460 | },
461 | {
462 | "name": "create_venv",
463 | "displayName": "Create Virtual Environment",
464 | "canBeReferencedInPrompt": true,
465 | "toolReferenceName": "uv-venv",
466 | "icon": "$(vm)",
467 | "userDescription": "Create a virtual environment with UV",
468 | "modelDescription": "Create a Python virtual environment using UV. Use when the user wants to set up an isolated Python environment, mentions virtual environments, or needs to create a clean development environment. Supports specifying Python version.",
469 | "inputSchema": {
470 | "type": "object",
471 | "properties": {
472 | "pythonVersion": {
473 | "type": "string",
474 | "description": "Specific Python version to use (optional, e.g., '3.11', '3.12')"
475 | }
476 | }
477 | }
478 | },
479 | {
480 | "name": "run_script",
481 | "displayName": "Run Python Script",
482 | "canBeReferencedInPrompt": true,
483 | "toolReferenceName": "uv-run",
484 | "icon": "$(play)",
485 | "userDescription": "Run a Python script with UV",
486 | "modelDescription": "Execute a Python script using UV's managed environment. Use when the user wants to run a Python file, mentions executing scripts, or needs to run code with project dependencies. Supports specifying Python version.",
487 | "inputSchema": {
488 | "type": "object",
489 | "properties": {
490 | "scriptPath": {
491 | "type": "string",
492 | "description": "Path to the Python script to run (required)"
493 | },
494 | "pythonVersion": {
495 | "type": "string",
496 | "description": "Specific Python version to use (optional, e.g., '3.11', '3.12')"
497 | }
498 | },
499 | "required": [
500 | "scriptPath"
501 | ]
502 | }
503 | },
504 | {
505 | "name": "add_script_dependency",
506 | "displayName": "Add Script Dependency",
507 | "canBeReferencedInPrompt": true,
508 | "toolReferenceName": "uv-script-dep",
509 | "icon": "$(link)",
510 | "userDescription": "Add inline dependency to a Python script",
511 | "modelDescription": "Add inline dependencies to a Python script file. Use when the user wants to add dependencies directly to a script file, mentions script-specific dependencies, or needs to create self-contained scripts with their own requirements.",
512 | "inputSchema": {
513 | "type": "object",
514 | "properties": {
515 | "scriptPath": {
516 | "type": "string",
517 | "description": "Path to the Python script file (required)"
518 | },
519 | "packageName": {
520 | "type": "string",
521 | "description": "Name of the package to add as dependency (required)"
522 | }
523 | },
524 | "required": [
525 | "scriptPath",
526 | "packageName"
527 | ]
528 | }
529 | },
530 | {
531 | "name": "install_python",
532 | "displayName": "Install Python Version",
533 | "canBeReferencedInPrompt": true,
534 | "toolReferenceName": "uv-python-install",
535 | "icon": "$(desktop-download)",
536 | "userDescription": "Install a specific Python version with UV",
537 | "modelDescription": "Install a specific Python version using UV's Python management. Use when the user needs a different Python version, mentions installing Python, or requires a specific Python version for their project.",
538 | "inputSchema": {
539 | "type": "object",
540 | "properties": {
541 | "pythonVersion": {
542 | "type": "string",
543 | "description": "Python version to install (required, e.g., '3.11', '3.12', '3.13')"
544 | }
545 | },
546 | "required": [
547 | "pythonVersion"
548 | ]
549 | }
550 | },
551 | {
552 | "name": "pin_python",
553 | "displayName": "Pin Python Version",
554 | "canBeReferencedInPrompt": true,
555 | "toolReferenceName": "uv-python-pin",
556 | "icon": "$(pin)",
557 | "userDescription": "Pin Python version for the current project",
558 | "modelDescription": "Pin a specific Python version for the current project. Use when the user wants to lock the Python version, mentions pinning Python, or needs to ensure consistent Python version across environments.",
559 | "inputSchema": {
560 | "type": "object",
561 | "properties": {
562 | "pythonVersion": {
563 | "type": "string",
564 | "description": "Python version to pin (required, e.g., '3.11', '3.12', '3.13')"
565 | }
566 | },
567 | "required": [
568 | "pythonVersion"
569 | ]
570 | }
571 | },
572 | {
573 | "name": "install_tool",
574 | "displayName": "Install Tool",
575 | "canBeReferencedInPrompt": true,
576 | "toolReferenceName": "uv-tool-install",
577 | "icon": "$(tools)",
578 | "userDescription": "Install a Python tool globally with UV",
579 | "modelDescription": "Install a Python tool globally using UV's tool management. Use when the user wants to install command-line tools like ruff, black, pytest, etc., mentions installing tools, or needs global Python utilities.",
580 | "inputSchema": {
581 | "type": "object",
582 | "properties": {
583 | "toolName": {
584 | "type": "string",
585 | "description": "Name of the tool to install (required, e.g., 'ruff', 'black', 'pytest')"
586 | }
587 | },
588 | "required": [
589 | "toolName"
590 | ]
591 | }
592 | },
593 | {
594 | "name": "run_tool",
595 | "displayName": "Run Tool with UVX",
596 | "canBeReferencedInPrompt": true,
597 | "toolReferenceName": "uvx-run",
598 | "icon": "$(terminal)",
599 | "userDescription": "Run a Python tool with UVX",
600 | "modelDescription": "Execute a Python tool using UVX (UV's tool runner). Use when the user wants to run Python tools without installing them globally, mentions running tools temporarily, or needs to execute command-line utilities.",
601 | "inputSchema": {
602 | "type": "object",
603 | "properties": {
604 | "toolName": {
605 | "type": "string",
606 | "description": "Name of the tool to run (required, e.g., 'ruff', 'black', 'pytest')"
607 | },
608 | "arguments": {
609 | "type": "string",
610 | "description": "Arguments to pass to the tool (optional)"
611 | }
612 | },
613 | "required": [
614 | "toolName"
615 | ]
616 | }
617 | },
618 | {
619 | "name": "activate_venv",
620 | "displayName": "Activate Virtual Environment",
621 | "canBeReferencedInPrompt": true,
622 | "toolReferenceName": "uv-activate-venv",
623 | "icon": "$(vm-active)",
624 | "userDescription": "Activate a virtual environment in a new terminal",
625 | "modelDescription": "Activate an existing virtual environment in a new terminal. Use when the user wants to activate a virtual environment, mentions activating venv, or needs to work within an isolated Python environment. The tool will automatically detect existing virtual environments in common locations (.venv, venv, .env, env) and activate the appropriate one.",
626 | "inputSchema": {
627 | "type": "object",
628 | "properties": {}
629 | }
630 | }
631 | ]
632 | },
633 | "scripts": {
634 | "vscode:prepublish": "tsc -p ./",
635 | "compile": "tsc -p ./",
636 | "watch": "tsc -watch -p ./",
637 | "pretest": "npm run compile",
638 | "test": "node ./out/test/runTest.js"
639 | },
640 | "devDependencies": {
641 | "@types/mocha": "^10.0.10",
642 | "@types/node": "^18.0.0",
643 | "@types/vscode": "^1.70.0",
644 | "@vscode/test-electron": "^2.3.0",
645 | "typescript": "^4.7.4"
646 | }
647 | }
648 |
--------------------------------------------------------------------------------
/src/commands.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { exec } from 'child_process';
3 | import * as path from 'path';
4 | import * as fs from 'fs';
5 |
6 | function runCommand(cmd: string) {
7 | vscode.window.showInformationMessage(`Running: ${cmd}`);
8 | exec(cmd, (error, stdout, stderr) => {
9 | if (error) {
10 | vscode.window.showErrorMessage(`Error: ${stderr}`);
11 | return;
12 | }
13 | vscode.window.showInformationMessage(stdout);
14 | });
15 | }
16 |
17 | export function registerCommands(context: vscode.ExtensionContext) {
18 | const commands = [
19 | { command: 'uv.init', callback: initProject },
20 | { command: 'uv.sync', callback: syncDependencies },
21 | { command: 'uv.upgrade', callback: () => runCommand('uv pip install --upgrade') },
22 | { command: 'uv.cache.clean', callback: () => runCommand('uv cache clean') },
23 | { command: 'uv.generateLock', callback: generateLockFile },
24 | { command: 'uv.upgradeDependencies', callback: upgradeDependencies },
25 | { command: 'uv.manageVirtualEnv', callback: manageVirtualEnv },
26 | { command: 'uv.activateVirtualEnv', callback: activateVirtualEnv },
27 | { command: 'uv.runScript', callback: runScript },
28 | { command: 'uv.addScriptDependency', callback: addScriptDependency },
29 | { command: 'uv.installPython', callback: installPython },
30 | { command: 'uv.pinPython', callback: pinPython },
31 | { command: 'uv.installTool', callback: installTool },
32 | { command: 'uv.runTool', callback: runTool },
33 | { command: 'uv.add', callback: addPackageToProject },
34 | { command: 'uv.addDev', callback: addDevPackageToProject }
35 | ];
36 |
37 | for (const { command, callback } of commands) {
38 | context.subscriptions.push(vscode.commands.registerCommand(command, callback));
39 | }
40 | }
41 |
42 | // Initialize a new Python project
43 | async function initProject() {
44 | const workspaceFolders = vscode.workspace.workspaceFolders;
45 | if (!workspaceFolders) {
46 | vscode.window.showErrorMessage('No workspace is open.');
47 | return;
48 | }
49 |
50 | const workspaceRoot = workspaceFolders[0].uri.fsPath;
51 |
52 | // Ask for project name
53 | const projectName = await vscode.window.showInputBox({
54 | placeHolder: 'Project name',
55 | prompt: 'Enter the name of the project',
56 | value: path.basename(workspaceRoot)
57 | });
58 |
59 | if (!projectName) return;
60 |
61 | // Show terminal and run the command
62 | const terminal = vscode.window.createTerminal('UV Init Project');
63 | terminal.show();
64 | terminal.sendText(`uv init ${projectName}`);
65 | }
66 |
67 | // Sync dependencies with advanced options
68 | async function syncDependencies() {
69 | const workspaceFolders = vscode.workspace.workspaceFolders;
70 | if (!workspaceFolders) {
71 | vscode.window.showErrorMessage('No workspace is open.');
72 | return;
73 | }
74 |
75 | const workspaceRoot = workspaceFolders[0].uri.fsPath;
76 |
77 | // Ask for sync options
78 | const options = await vscode.window.showQuickPick([
79 | { label: 'Basic sync', description: 'Sync dependencies with default options' },
80 | { label: 'Sync from specific file', description: 'Sync dependencies from a specific requirements file' },
81 | { label: 'Sync specific groups', description: 'Sync specific dependency groups' }
82 | ], {
83 | placeHolder: 'Select sync options'
84 | });
85 |
86 | if (!options) return;
87 |
88 | let command = 'uv sync';
89 |
90 | if (options.label === 'Sync from specific file') {
91 | // Get requirements files in the workspace
92 | const files = await vscode.workspace.findFiles('**/requirements*.txt', '**/venv/**');
93 |
94 | if (files.length === 0) {
95 | vscode.window.showErrorMessage('No requirements files found in the workspace.');
96 | return;
97 | }
98 |
99 | // Get relative paths for display
100 | const relativePaths = files.map(file => {
101 | return path.relative(workspaceRoot, file.fsPath);
102 | });
103 |
104 | // Ask user to select a requirements file
105 | const selectedFile = await vscode.window.showQuickPick(relativePaths, {
106 | placeHolder: 'Select a requirements file'
107 | });
108 |
109 | if (!selectedFile) return;
110 |
111 | command += ` ${selectedFile}`;
112 | } else if (options.label === 'Sync specific groups') {
113 | const groups = await vscode.window.showInputBox({
114 | placeHolder: 'Enter groups (comma-separated, e.g. dev,test)',
115 | prompt: 'Specify which dependency groups to sync'
116 | });
117 |
118 | if (groups) {
119 | const groupsList = groups.split(',').map(g => g.trim());
120 | for (const group of groupsList) {
121 | command += ` --group ${group}`;
122 | }
123 | }
124 | }
125 |
126 | // Show terminal and run the command
127 | const terminal = vscode.window.createTerminal('UV Sync');
128 | terminal.show();
129 | terminal.sendText(command);
130 | }
131 |
132 | // Add a package to the project
133 | async function addPackageToProject() {
134 | const workspaceFolders = vscode.workspace.workspaceFolders;
135 | if (!workspaceFolders) {
136 | vscode.window.showErrorMessage('No workspace is open.');
137 | return;
138 | }
139 |
140 | const workspaceRoot = workspaceFolders[0].uri.fsPath;
141 |
142 | // Ask for package name
143 | const packageName = await vscode.window.showInputBox({
144 | placeHolder: 'Package name',
145 | prompt: 'Enter the package name to add'
146 | });
147 |
148 | if (!packageName) return;
149 |
150 | // Ask for package version (optional)
151 | const packageVersion = await vscode.window.showInputBox({
152 | placeHolder: 'Version constraint (optional, e.g. >=1.0.0)',
153 | prompt: 'Enter version constraint (optional)'
154 | });
155 |
156 | // Ask for extras (optional)
157 | const extras = await vscode.window.showInputBox({
158 | placeHolder: 'Extras (optional, e.g. dev,test)',
159 | prompt: 'Enter extras to include (optional)'
160 | });
161 |
162 | // Build command
163 | let command = `uv add ${packageName}`;
164 |
165 | if (packageVersion) {
166 | command += `==${packageVersion}`;
167 | }
168 |
169 | if (extras) {
170 | command += `[${extras}]`;
171 | }
172 |
173 | // Show terminal and run the command
174 | const terminal = vscode.window.createTerminal('UV Add Package');
175 | terminal.show();
176 | terminal.sendText(command);
177 | }
178 |
179 | // Add a dev package to the project
180 | async function addDevPackageToProject() {
181 | const workspaceFolders = vscode.workspace.workspaceFolders;
182 | if (!workspaceFolders) {
183 | vscode.window.showErrorMessage('No workspace is open.');
184 | return;
185 | }
186 |
187 | const workspaceRoot = workspaceFolders[0].uri.fsPath;
188 |
189 | // Ask for package name
190 | const packageName = await vscode.window.showInputBox({
191 | placeHolder: 'Package name',
192 | prompt: 'Enter the package name to add as a dev dependency'
193 | });
194 |
195 | if (!packageName) return;
196 |
197 | // Ask for package version (optional)
198 | const packageVersion = await vscode.window.showInputBox({
199 | placeHolder: 'Version constraint (optional, e.g. >=1.0.0)',
200 | prompt: 'Enter version constraint (optional)'
201 | });
202 |
203 | // Ask for extras (optional)
204 | const extras = await vscode.window.showInputBox({
205 | placeHolder: 'Extras (optional, e.g. dev,test)',
206 | prompt: 'Enter extras to include (optional)'
207 | });
208 |
209 | // Build command with --dev flag
210 | let command = `uv add --dev ${packageName}`;
211 |
212 | if (packageVersion) {
213 | command += `==${packageVersion}`;
214 | }
215 |
216 | if (extras) {
217 | command += `[${extras}]`;
218 | }
219 |
220 | // Show terminal and run the command
221 | const terminal = vscode.window.createTerminal('UV Add Dev Package');
222 | terminal.show();
223 | terminal.sendText(command);
224 | }
225 |
226 | // Add inline dependencies to a script
227 | async function addScriptDependency() {
228 | const workspaceFolders = vscode.workspace.workspaceFolders;
229 | if (!workspaceFolders) {
230 | vscode.window.showErrorMessage('No workspace is open.');
231 | return;
232 | }
233 |
234 | const workspaceRoot = workspaceFolders[0].uri.fsPath;
235 |
236 | // Get Python files in the workspace
237 | const files = await vscode.workspace.findFiles('**/*.py', '**/venv/**');
238 |
239 | if (files.length === 0) {
240 | vscode.window.showErrorMessage('No Python files found in the workspace.');
241 | return;
242 | }
243 |
244 | // Get relative paths for display
245 | const relativePaths = files.map(file => {
246 | return path.relative(workspaceRoot, file.fsPath);
247 | });
248 |
249 | // Ask user to select a script
250 | const selectedScript = await vscode.window.showQuickPick(relativePaths, {
251 | placeHolder: 'Select a Python script'
252 | });
253 |
254 | if (!selectedScript) return;
255 |
256 | // Ask for package name
257 | const packageName = await vscode.window.showInputBox({
258 | placeHolder: 'Package name',
259 | prompt: 'Enter the package name to add as a dependency'
260 | });
261 |
262 | if (!packageName) return;
263 |
264 | // Show terminal and run the command
265 | const terminal = vscode.window.createTerminal('UV Add Script Dependency');
266 | terminal.show();
267 | terminal.sendText(`uv add --script ${selectedScript} ${packageName}`);
268 | }
269 |
270 | // Install Python versions
271 | async function installPython() {
272 | // Ask for Python version
273 | const pythonVersion = await vscode.window.showInputBox({
274 | placeHolder: 'Python version (e.g. 3.11, 3.12)',
275 | prompt: 'Enter Python version to install'
276 | });
277 |
278 | if (!pythonVersion) return;
279 |
280 | // Show terminal and run the command
281 | const terminal = vscode.window.createTerminal('UV Python Install');
282 | terminal.show();
283 | terminal.sendText(`uv python install ${pythonVersion}`);
284 | }
285 |
286 | // Pin Python version for the current project
287 | async function pinPython() {
288 | const workspaceFolders = vscode.workspace.workspaceFolders;
289 | if (!workspaceFolders) {
290 | vscode.window.showErrorMessage('No workspace is open.');
291 | return;
292 | }
293 |
294 | // Ask for Python version
295 | const pythonVersion = await vscode.window.showInputBox({
296 | placeHolder: 'Python version (e.g. 3.11, 3.12)',
297 | prompt: 'Enter Python version to pin'
298 | });
299 |
300 | if (!pythonVersion) return;
301 |
302 | // Show terminal and run the command
303 | const terminal = vscode.window.createTerminal('UV Python Pin');
304 | terminal.show();
305 | terminal.sendText(`uv python pin ${pythonVersion}`);
306 | }
307 |
308 | // Install a tool with uv
309 | async function installTool() {
310 | // Ask for tool name
311 | const toolName = await vscode.window.showInputBox({
312 | placeHolder: 'Tool name (e.g. ruff, black)',
313 | prompt: 'Enter the name of the tool to install'
314 | });
315 |
316 | if (!toolName) return;
317 |
318 | // Show terminal and run the command
319 | const terminal = vscode.window.createTerminal('UV Tool Install');
320 | terminal.show();
321 | terminal.sendText(`uv tool install ${toolName}`);
322 | }
323 |
324 | // Run a tool with uvx
325 | async function runTool() {
326 | // Ask for tool name
327 | const toolName = await vscode.window.showInputBox({
328 | placeHolder: 'Tool name (e.g. ruff, black)',
329 | prompt: 'Enter the name of the tool to run'
330 | });
331 |
332 | if (!toolName) return;
333 |
334 | // Ask for tool arguments
335 | const toolArgs = await vscode.window.showInputBox({
336 | placeHolder: 'Arguments (optional)',
337 | prompt: 'Enter arguments for the tool (optional)'
338 | });
339 |
340 | // Show terminal and run the command
341 | const terminal = vscode.window.createTerminal('UVX Run Tool');
342 | terminal.show();
343 | terminal.sendText(`uvx ${toolName}${toolArgs ? ' ' + toolArgs : ''}`);
344 | }
345 |
346 | // Upgrade dependencies
347 | async function upgradeDependencies() {
348 | const workspaceFolders = vscode.workspace.workspaceFolders;
349 | if (!workspaceFolders) {
350 | vscode.window.showErrorMessage('No workspace is open.');
351 | return;
352 | }
353 |
354 | const workspaceRoot = workspaceFolders[0].uri.fsPath;
355 | const pyprojectPath = path.join(workspaceRoot, 'pyproject.toml');
356 |
357 | if (!fs.existsSync(pyprojectPath)) {
358 | vscode.window.showErrorMessage('pyproject.toml file not found.');
359 | return;
360 | }
361 |
362 | // Ask for upgrade options
363 | const options = await vscode.window.showQuickPick([
364 | { label: 'Upgrade all', description: 'Upgrade all dependencies to their latest versions' },
365 | { label: 'Upgrade specific package', description: 'Upgrade a specific package to its latest version' }
366 | ], {
367 | placeHolder: 'Select upgrade options'
368 | });
369 |
370 | if (!options) return;
371 |
372 | let command = 'uv pip compile pyproject.toml -o uv.lock';
373 |
374 | if (options.label === 'Upgrade all') {
375 | command += ' --upgrade';
376 | } else if (options.label === 'Upgrade specific package') {
377 | // Parse pyproject.toml to get the list of dependencies
378 | const pyprojectContent = fs.readFileSync(pyprojectPath, 'utf-8');
379 | const depMatches = pyprojectContent.match(/\[dependencies\](.*?)(\n\[|\n*$)/s);
380 |
381 | if (!depMatches || !depMatches[1]) {
382 | vscode.window.showErrorMessage('No dependencies found in pyproject.toml');
383 | return;
384 | }
385 |
386 | const depsSection = depMatches[1];
387 | const packageRegex = /([a-zA-Z0-9_-]+)\s*=\s*["'][^"']*["']/g;
388 | const packages: string[] = [];
389 |
390 | let match;
391 | while ((match = packageRegex.exec(depsSection)) !== null) {
392 | packages.push(match[1]);
393 | }
394 |
395 | if (packages.length === 0) {
396 | vscode.window.showErrorMessage('No packages found in dependencies.');
397 | return;
398 | }
399 |
400 | const selectedPackage = await vscode.window.showQuickPick(packages, {
401 | placeHolder: 'Select a package to upgrade'
402 | });
403 |
404 | if (!selectedPackage) return;
405 |
406 | command += ` --upgrade-package ${selectedPackage}`;
407 | }
408 |
409 | // Show progress notification
410 | vscode.window.withProgress({
411 | location: vscode.ProgressLocation.Notification,
412 | title: 'Upgrading dependencies',
413 | cancellable: false
414 | }, async (progress) => {
415 | try {
416 | // Run uv pip compile command to upgrade dependencies
417 | await new Promise((resolve, reject) => {
418 | exec(command, { cwd: workspaceRoot }, (error, stdout, stderr) => {
419 | if (error) {
420 | reject(new Error(stderr));
421 | return;
422 | }
423 | resolve();
424 | });
425 | });
426 |
427 | vscode.window.showInformationMessage('Dependencies upgraded successfully.');
428 | } catch (error: any) {
429 | vscode.window.showErrorMessage(`Failed to upgrade dependencies: ${error.message}`);
430 | }
431 | });
432 | }
433 |
434 | // Create and manage virtual environments
435 | async function manageVirtualEnv() {
436 | const workspaceFolders = vscode.workspace.workspaceFolders;
437 | if (!workspaceFolders) {
438 | vscode.window.showErrorMessage('No workspace is open.');
439 | return;
440 | }
441 |
442 | const workspaceRoot = workspaceFolders[0].uri.fsPath;
443 |
444 | // Ask for venv options
445 | const options = await vscode.window.showQuickPick([
446 | { label: 'Create venv', description: 'Create a new virtual environment' },
447 | { label: 'Create venv with specific Python', description: 'Create a virtual environment with a specific Python version' }
448 | ], {
449 | placeHolder: 'Select virtual environment options'
450 | });
451 |
452 | if (!options) return;
453 |
454 | let command = 'uv venv';
455 |
456 | if (options.label === 'Create venv with specific Python') {
457 | const pythonVersion = await vscode.window.showInputBox({
458 | placeHolder: 'Enter Python version (e.g. 3.11, 3.12)',
459 | prompt: 'Specify which Python version to use'
460 | });
461 |
462 | if (pythonVersion) {
463 | command += ` --python ${pythonVersion}`;
464 | }
465 | }
466 |
467 | // Show progress notification
468 | vscode.window.withProgress({
469 | location: vscode.ProgressLocation.Notification,
470 | title: 'Creating virtual environment',
471 | cancellable: false
472 | }, async (progress) => {
473 | try {
474 | // Run uv venv command to create virtual environment
475 | await new Promise((resolve, reject) => {
476 | exec(command, { cwd: workspaceRoot }, (error, stdout, stderr) => {
477 | if (error) {
478 | reject(new Error(stderr));
479 | return;
480 | }
481 | resolve();
482 | });
483 | });
484 |
485 | vscode.window.showInformationMessage('Virtual environment created successfully.');
486 | } catch (error: any) {
487 | vscode.window.showErrorMessage(`Failed to create virtual environment: ${error.message}`);
488 | }
489 | });
490 | }
491 |
492 | // Run Python scripts with uv
493 | async function runScript() {
494 | const workspaceFolders = vscode.workspace.workspaceFolders;
495 | if (!workspaceFolders) {
496 | vscode.window.showErrorMessage('No workspace is open.');
497 | return;
498 | }
499 |
500 | const workspaceRoot = workspaceFolders[0].uri.fsPath;
501 |
502 | // Get Python files in the workspace
503 | const files = await vscode.workspace.findFiles('**/*.py', '**/venv/**');
504 |
505 | if (files.length === 0) {
506 | vscode.window.showErrorMessage('No Python files found in the workspace.');
507 | return;
508 | }
509 |
510 | // Get relative paths for display
511 | const relativePaths = files.map(file => {
512 | return path.relative(workspaceRoot, file.fsPath);
513 | });
514 |
515 | // Ask user to select a script
516 | const selectedScript = await vscode.window.showQuickPick(relativePaths, {
517 | placeHolder: 'Select a Python script to run'
518 | });
519 |
520 | if (!selectedScript) return;
521 |
522 | // Ask for Python version (optional)
523 | const usePythonVersion = await vscode.window.showQuickPick(['Use default Python', 'Specify Python version'], {
524 | placeHolder: 'Select Python version option'
525 | });
526 |
527 | let command = 'uv run';
528 |
529 | if (usePythonVersion === 'Specify Python version') {
530 | const pythonVersion = await vscode.window.showInputBox({
531 | placeHolder: 'Enter Python version (e.g. 3.11, 3.12)',
532 | prompt: 'Specify which Python version to use'
533 | });
534 |
535 | if (pythonVersion) {
536 | command += ` --python ${pythonVersion}`;
537 | }
538 | }
539 |
540 | command += ` ${selectedScript}`;
541 |
542 | // Show terminal and run the command
543 | const terminal = vscode.window.createTerminal('UV Run');
544 | terminal.show();
545 | terminal.sendText(command);
546 | }
547 |
548 | // Activate virtual environment
549 | async function activateVirtualEnv() {
550 | const workspaceFolders = vscode.workspace.workspaceFolders;
551 | if (!workspaceFolders) {
552 | vscode.window.showErrorMessage('No workspace is open.');
553 | return;
554 | }
555 |
556 | const workspaceRoot = workspaceFolders[0].uri.fsPath;
557 |
558 | // Common virtual environment paths to check
559 | const commonVenvPaths = [
560 | path.join(workspaceRoot, '.venv'),
561 | path.join(workspaceRoot, 'venv'),
562 | path.join(workspaceRoot, '.env'),
563 | path.join(workspaceRoot, 'env')
564 | ];
565 |
566 | // Find existing virtual environments
567 | const existingVenvs: string[] = [];
568 | for (const venvPath of commonVenvPaths) {
569 | if (fs.existsSync(venvPath)) {
570 | // Check if it's a valid virtual environment
571 | const activateScript = process.platform === 'win32'
572 | ? path.join(venvPath, 'Scripts', 'activate.bat')
573 | : path.join(venvPath, 'bin', 'activate');
574 |
575 | if (fs.existsSync(activateScript)) {
576 | existingVenvs.push(venvPath);
577 | }
578 | }
579 | }
580 |
581 | let selectedVenvPath: string;
582 |
583 | if (existingVenvs.length === 0) {
584 | // No virtual environments found, ask if user wants to create one
585 | const createVenv = await vscode.window.showQuickPick([
586 | { label: 'Create new virtual environment', description: 'Create a new virtual environment and activate it' },
587 | { label: 'Specify custom path', description: 'Specify a custom path to an existing virtual environment' }
588 | ], {
589 | placeHolder: 'No virtual environments found. What would you like to do?'
590 | });
591 |
592 | if (!createVenv) return;
593 |
594 | if (createVenv.label === 'Create new virtual environment') {
595 | // Create a new virtual environment first
596 | await manageVirtualEnv();
597 | // After creation, try to find the newly created venv
598 | const defaultVenvPath = path.join(workspaceRoot, '.venv');
599 | if (fs.existsSync(defaultVenvPath)) {
600 | selectedVenvPath = defaultVenvPath;
601 | } else {
602 | vscode.window.showErrorMessage('Failed to create virtual environment.');
603 | return;
604 | }
605 | } else {
606 | // Ask for custom path
607 | const customPath = await vscode.window.showInputBox({
608 | placeHolder: 'Enter path to virtual environment',
609 | prompt: 'Enter the full path to your virtual environment directory'
610 | });
611 |
612 | if (!customPath || !fs.existsSync(customPath)) {
613 | vscode.window.showErrorMessage('Invalid virtual environment path.');
614 | return;
615 | }
616 |
617 | selectedVenvPath = customPath;
618 | }
619 | } else if (existingVenvs.length === 1) {
620 | // Only one virtual environment found, use it
621 | selectedVenvPath = existingVenvs[0];
622 | } else {
623 | // Multiple virtual environments found, let user choose
624 | const venvOptions = existingVenvs.map(venvPath => ({
625 | label: path.basename(venvPath),
626 | description: venvPath,
627 | detail: `Virtual environment at ${venvPath}`
628 | }));
629 |
630 | const selectedVenv = await vscode.window.showQuickPick(venvOptions, {
631 | placeHolder: 'Select a virtual environment to activate'
632 | });
633 |
634 | if (!selectedVenv) return;
635 | selectedVenvPath = selectedVenv.description;
636 | }
637 |
638 | // Determine the activation command based on the platform
639 | let activationCommand: string;
640 | if (process.platform === 'win32') {
641 | // Windows
642 | const activateScript = path.join(selectedVenvPath, 'Scripts', 'activate.bat');
643 | activationCommand = `"${activateScript}"`;
644 | } else {
645 | // Unix-like systems (macOS, Linux)
646 | const activateScript = path.join(selectedVenvPath, 'bin', 'activate');
647 | activationCommand = `source "${activateScript}"`;
648 | }
649 |
650 | // Create a new terminal and activate the virtual environment
651 | const terminal = vscode.window.createTerminal({
652 | name: 'UV Virtual Environment',
653 | cwd: workspaceRoot
654 | });
655 |
656 | terminal.show();
657 | terminal.sendText(activationCommand);
658 |
659 | // Show success message
660 | vscode.window.showInformationMessage(`Virtual environment activated: ${path.basename(selectedVenvPath)}`);
661 | }
662 |
663 | // Generate uv.lock file from pyproject.toml with advanced options
664 | async function generateLockFile() {
665 | const workspaceFolders = vscode.workspace.workspaceFolders;
666 | if (!workspaceFolders) {
667 | vscode.window.showErrorMessage('No workspace is open.');
668 | return;
669 | }
670 |
671 | const workspaceRoot = workspaceFolders[0].uri.fsPath;
672 | const pyprojectPath = path.join(workspaceRoot, 'pyproject.toml');
673 |
674 | if (!fs.existsSync(pyprojectPath)) {
675 | vscode.window.showErrorMessage('pyproject.toml file not found.');
676 | return;
677 | }
678 |
679 | // Ask for compile options
680 | const options = await vscode.window.showQuickPick([
681 | { label: 'Basic compile', description: 'Generate lock file with default options' },
682 | { label: 'Include all extras', description: 'Include all optional dependencies' },
683 | { label: 'Specify extras', description: 'Include specific optional dependencies' },
684 | { label: 'Specify groups', description: 'Compile specific dependency groups' }
685 | ], {
686 | placeHolder: 'Select compile options'
687 | });
688 |
689 | if (!options) return;
690 |
691 | let command = 'uv pip compile pyproject.toml -o uv.lock';
692 |
693 | if (options.label === 'Include all extras') {
694 | command += ' --all-extras';
695 | } else if (options.label === 'Specify extras') {
696 | const extras = await vscode.window.showInputBox({
697 | placeHolder: 'Enter extras (comma-separated, e.g. dev,test)',
698 | prompt: 'Specify which extras to include'
699 | });
700 |
701 | if (extras) {
702 | const extrasList = extras.split(',').map(e => e.trim());
703 | for (const extra of extrasList) {
704 | command += ` --extra ${extra}`;
705 | }
706 | }
707 | } else if (options.label === 'Specify groups') {
708 | const groups = await vscode.window.showInputBox({
709 | placeHolder: 'Enter groups (comma-separated, e.g. dev,test)',
710 | prompt: 'Specify which dependency groups to include'
711 | });
712 |
713 | if (groups) {
714 | const groupsList = groups.split(',').map(g => g.trim());
715 | for (const group of groupsList) {
716 | command += ` --group ${group}`;
717 | }
718 | }
719 | }
720 |
721 | // Show progress notification
722 | vscode.window.withProgress({
723 | location: vscode.ProgressLocation.Notification,
724 | title: 'Generating uv.lock file',
725 | cancellable: false
726 | }, async (progress) => {
727 | try {
728 | // Run uv pip compile command to generate lock file
729 | await new Promise((resolve, reject) => {
730 | exec(command, { cwd: workspaceRoot }, (error, stdout, stderr) => {
731 | if (error) {
732 | reject(new Error(stderr));
733 | return;
734 | }
735 | resolve();
736 | });
737 | });
738 |
739 | vscode.window.showInformationMessage('uv.lock file generated successfully.');
740 | } catch (error: any) {
741 | vscode.window.showErrorMessage(`Failed to generate uv.lock file: ${error.message}`);
742 | }
743 | });
744 | }
745 |
--------------------------------------------------------------------------------
/src/languageModelTools.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { exec } from 'child_process';
3 | import * as path from 'path';
4 | import * as fs from 'fs';
5 |
6 | // Tool parameter interfaces
7 | export interface InitProjectParams {
8 | projectName?: string;
9 | }
10 |
11 | export interface SyncDependenciesParams {
12 | syncType?: 'basic' | 'from-file' | 'groups';
13 | requirementsFile?: string;
14 | groups?: string;
15 | }
16 |
17 | export interface AddPackageParams {
18 | packageName: string;
19 | version?: string;
20 | extras?: string;
21 | }
22 |
23 | export interface AddDevPackageParams {
24 | packageName: string;
25 | version?: string;
26 | extras?: string;
27 | }
28 |
29 | export interface UpgradePackagesParams {
30 | upgradeType?: 'all' | 'specific';
31 | packageName?: string;
32 | }
33 |
34 | export interface CleanCacheParams {
35 | // No parameters needed
36 | }
37 |
38 | export interface GenerateLockParams {
39 | includeExtras?: 'none' | 'all' | 'specific';
40 | extras?: string;
41 | groups?: string;
42 | }
43 |
44 | export interface CreateVenvParams {
45 | pythonVersion?: string;
46 | }
47 |
48 | export interface RunScriptParams {
49 | scriptPath: string;
50 | pythonVersion?: string;
51 | }
52 |
53 | export interface AddScriptDependencyParams {
54 | scriptPath: string;
55 | packageName: string;
56 | }
57 |
58 | export interface InstallPythonParams {
59 | pythonVersion: string;
60 | }
61 |
62 | export interface PinPythonParams {
63 | pythonVersion: string;
64 | }
65 |
66 | export interface InstallToolParams {
67 | toolName: string;
68 | }
69 |
70 | export interface RunToolParams {
71 | toolName: string;
72 | arguments?: string;
73 | }
74 |
75 | export interface ActivateVirtualEnvParams {
76 | // No parameters needed - the tool will auto-detect or prompt for venv selection
77 | }
78 |
79 | // Base class for UV tools
80 | abstract class UVToolBase implements vscode.LanguageModelTool {
81 | protected workspaceRoot: string | undefined;
82 | protected currentWorkingDirectory: string | undefined;
83 |
84 | constructor() {
85 | const workspaceFolders = vscode.workspace.workspaceFolders;
86 | this.workspaceRoot = workspaceFolders ? workspaceFolders[0].uri.fsPath : undefined;
87 |
88 | // Get the directory of the currently active file, if any
89 | const activeEditor = vscode.window.activeTextEditor;
90 | if (activeEditor && activeEditor.document.uri.scheme === 'file') {
91 | this.currentWorkingDirectory = path.dirname(activeEditor.document.uri.fsPath);
92 | } else {
93 | this.currentWorkingDirectory = this.workspaceRoot;
94 | }
95 | }
96 |
97 | protected async selectWorkingDirectory(): Promise {
98 | if (!this.workspaceRoot) {
99 | throw new Error('No workspace is open. Please open a folder or workspace to use UV tools.');
100 | }
101 |
102 | // If current working directory is the same as workspace root, no need to ask
103 | if (this.currentWorkingDirectory === this.workspaceRoot) {
104 | return this.workspaceRoot;
105 | }
106 |
107 | // If we have multiple potential directories, ask the user
108 | if (this.currentWorkingDirectory && this.currentWorkingDirectory !== this.workspaceRoot) {
109 | const workspaceRelative = path.relative(this.workspaceRoot, this.currentWorkingDirectory);
110 |
111 | const choice = await vscode.window.showQuickPick([
112 | {
113 | label: `Workspace Root`,
114 | description: this.workspaceRoot,
115 | detail: 'Use the root directory of the workspace'
116 | },
117 | {
118 | label: `Current Folder`,
119 | description: this.currentWorkingDirectory,
120 | detail: `Use the current folder (${workspaceRelative})`
121 | }
122 | ], {
123 | placeHolder: 'Select directory for UV operation',
124 | title: 'Choose Working Directory'
125 | });
126 |
127 | if (!choice) {
128 | throw new Error('Operation cancelled: No directory selected');
129 | }
130 |
131 | return choice.description!;
132 | }
133 |
134 | return this.workspaceRoot;
135 | }
136 |
137 | protected checkWorkspace(): boolean {
138 | if (!this.workspaceRoot) {
139 | throw new Error('No workspace is open. Please open a folder or workspace to use UV tools.');
140 | }
141 | return true;
142 | }
143 |
144 | protected async executeCommand(command: string, workingDir?: string): Promise {
145 | const cwd = workingDir || await this.selectWorkingDirectory();
146 | return new Promise((resolve, reject) => {
147 | exec(command, { cwd }, (error: any, stdout: any, stderr: any) => {
148 | if (error) {
149 | reject(new Error(`Command failed: ${stderr || error.message}`));
150 | return;
151 | }
152 | resolve(stdout.trim());
153 | });
154 | });
155 | }
156 |
157 | protected async createTerminalAndRun(command: string, terminalName: string, workingDir?: string): Promise {
158 | const cwd = workingDir || await this.selectWorkingDirectory();
159 | const terminal = vscode.window.createTerminal({
160 | name: terminalName
161 | });
162 | terminal.show();
163 |
164 | // Change to the working directory explicitly and then run the command
165 | // Use PowerShell compatible commands since we're on Windows
166 | const cdCommand = `Set-Location "${cwd}"`;
167 | const pwdCommand = `Write-Host "Working directory: $(Get-Location)"`;
168 | const fullCommand = `${cdCommand}; ${pwdCommand}; ${command}`;
169 |
170 | terminal.sendText(fullCommand);
171 | }
172 |
173 | abstract prepareInvocation(
174 | options: vscode.LanguageModelToolInvocationPrepareOptions,
175 | token: vscode.CancellationToken
176 | ): vscode.ProviderResult;
177 |
178 | abstract invoke(
179 | options: vscode.LanguageModelToolInvocationOptions,
180 | token: vscode.CancellationToken
181 | ): vscode.ProviderResult;
182 | }
183 |
184 | // Tool implementations
185 | export class InitProjectTool extends UVToolBase {
186 | async prepareInvocation(
187 | options: vscode.LanguageModelToolInvocationPrepareOptions,
188 | _token: vscode.CancellationToken
189 | ): Promise {
190 | this.checkWorkspace();
191 | const selectedDir = await this.selectWorkingDirectory();
192 | const projectName = options.input.projectName || path.basename(selectedDir);
193 |
194 | return {
195 | invocationMessage: `Initializing UV project: ${projectName}`,
196 | confirmationMessages: {
197 | title: 'Initialize UV Project',
198 | message: new vscode.MarkdownString(
199 | `Initialize a new Python project with UV?\n\n` +
200 | `**Project name:** \`${projectName}\`\n` +
201 | `**Directory:** \`${selectedDir}\`\n\n` +
202 | `This will create a \`pyproject.toml\` file and set up the project structure.`
203 | )
204 | }
205 | };
206 | }
207 |
208 | async invoke(
209 | options: vscode.LanguageModelToolInvocationOptions,
210 | _token: vscode.CancellationToken
211 | ): Promise {
212 | try {
213 | this.checkWorkspace();
214 | const selectedDir = await this.selectWorkingDirectory();
215 | const projectName = options.input.projectName || path.basename(selectedDir);
216 | const command = `uv init ${projectName}`;
217 |
218 | await this.createTerminalAndRun(command, 'UV Init Project', selectedDir);
219 |
220 | return new vscode.LanguageModelToolResult([
221 | new vscode.LanguageModelTextPart(`Successfully initiated UV project initialization for "${projectName}" in ${selectedDir}. Check the terminal for progress and results.`)
222 | ]);
223 | } catch (error: any) {
224 | throw new Error(`Failed to initialize project: ${error.message}`);
225 | }
226 | }
227 | }
228 |
229 | export class SyncDependenciesTool extends UVToolBase {
230 | async prepareInvocation(
231 | options: vscode.LanguageModelToolInvocationPrepareOptions,
232 | _token: vscode.CancellationToken
233 | ): Promise {
234 | this.checkWorkspace();
235 | const selectedDir = await this.selectWorkingDirectory();
236 | const { syncType = 'basic', requirementsFile, groups } = options.input;
237 |
238 | let description = 'Sync dependencies with default options';
239 | if (syncType === 'from-file' && requirementsFile) {
240 | description = `Sync from requirements file: ${requirementsFile}`;
241 | } else if (syncType === 'groups' && groups) {
242 | description = `Sync specific groups: ${groups}`;
243 | }
244 |
245 | return {
246 | invocationMessage: 'Syncing project dependencies',
247 | confirmationMessages: {
248 | title: 'Sync Dependencies',
249 | message: new vscode.MarkdownString(
250 | `Sync project dependencies using UV?\n\n` +
251 | `**Operation:** ${description}\n` +
252 | `**Directory:** \`${selectedDir}\`\n\n` +
253 | `This will install or update dependencies based on your project configuration.`
254 | )
255 | }
256 | };
257 | }
258 |
259 | async invoke(
260 | options: vscode.LanguageModelToolInvocationOptions,
261 | _token: vscode.CancellationToken
262 | ): Promise {
263 | try {
264 | this.checkWorkspace();
265 | const selectedDir = await this.selectWorkingDirectory();
266 | const { syncType = 'basic', requirementsFile, groups } = options.input;
267 |
268 | let command = 'uv sync';
269 |
270 | if (syncType === 'from-file' && requirementsFile) {
271 | command += ` ${requirementsFile}`;
272 | } else if (syncType === 'groups' && groups) {
273 | const groupsList = groups.split(',').map((g: string) => g.trim());
274 | for (const group of groupsList) {
275 | command += ` --group ${group}`;
276 | }
277 | }
278 |
279 | await this.createTerminalAndRun(command, 'UV Sync', selectedDir);
280 |
281 | return new vscode.LanguageModelToolResult([
282 | new vscode.LanguageModelTextPart(`Successfully initiated dependency sync. Check the terminal for progress and results.`)
283 | ]);
284 | } catch (error: any) {
285 | throw new Error(`Failed to sync dependencies: ${error.message}`);
286 | }
287 | }
288 | }
289 |
290 | export class AddPackageTool extends UVToolBase {
291 | async prepareInvocation(
292 | options: vscode.LanguageModelToolInvocationPrepareOptions,
293 | _token: vscode.CancellationToken
294 | ): Promise {
295 | this.checkWorkspace();
296 | const selectedDir = await this.selectWorkingDirectory();
297 | const { packageName, version, extras } = options.input;
298 |
299 | let packageSpec = packageName;
300 | if (version) packageSpec += `==${version}`;
301 | if (extras) packageSpec += `[${extras}]`;
302 |
303 | return {
304 | invocationMessage: `Adding package: ${packageSpec}`,
305 | confirmationMessages: {
306 | title: 'Add Package',
307 | message: new vscode.MarkdownString(
308 | `Add Python package to the project?\n\n` +
309 | `**Package:** \`${packageSpec}\`\n` +
310 | `**Directory:** \`${selectedDir}\`\n\n` +
311 | `This will add the package to your \`pyproject.toml\` and install it.`
312 | )
313 | }
314 | };
315 | }
316 |
317 | async invoke(
318 | options: vscode.LanguageModelToolInvocationOptions,
319 | _token: vscode.CancellationToken
320 | ): Promise {
321 | try {
322 | this.checkWorkspace();
323 | const selectedDir = await this.selectWorkingDirectory();
324 | const { packageName, version, extras } = options.input;
325 |
326 | let command = `uv add ${packageName}`;
327 | if (version) command += `==${version}`;
328 | if (extras) command += `[${extras}]`;
329 |
330 | await this.createTerminalAndRun(command, 'UV Add Package', selectedDir);
331 |
332 | return new vscode.LanguageModelToolResult([
333 | new vscode.LanguageModelTextPart(`Successfully initiated package addition for "${packageName}". Check the terminal for progress and results.`)
334 | ]);
335 | } catch (error: any) {
336 | throw new Error(`Failed to add package: ${error.message}`);
337 | }
338 | }
339 | }
340 |
341 | export class AddDevPackageTool extends UVToolBase {
342 | async prepareInvocation(
343 | options: vscode.LanguageModelToolInvocationPrepareOptions,
344 | _token: vscode.CancellationToken
345 | ): Promise {
346 | this.checkWorkspace();
347 | const selectedDir = await this.selectWorkingDirectory();
348 | const { packageName, version, extras } = options.input;
349 |
350 | let packageSpec = packageName;
351 | if (version) packageSpec += `==${version}`;
352 | if (extras) packageSpec += `[${extras}]`;
353 |
354 | return {
355 | invocationMessage: `Adding dev package: ${packageSpec}`,
356 | confirmationMessages: {
357 | title: 'Add Dev Package',
358 | message: new vscode.MarkdownString(
359 | `Add Python package as a development dependency?\n\n` +
360 | `**Package:** \`${packageSpec}\`\n` +
361 | `**Directory:** \`${selectedDir}\`\n\n` +
362 | `This will add the package to your \`pyproject.toml\` dev dependencies and install it.`
363 | )
364 | }
365 | };
366 | }
367 |
368 | async invoke(
369 | options: vscode.LanguageModelToolInvocationOptions,
370 | _token: vscode.CancellationToken
371 | ): Promise {
372 | try {
373 | this.checkWorkspace();
374 | const selectedDir = await this.selectWorkingDirectory();
375 | const { packageName, version, extras } = options.input;
376 |
377 | let command = `uv add --dev ${packageName}`;
378 | if (version) command += `==${version}`;
379 | if (extras) command += `[${extras}]`;
380 |
381 | await this.createTerminalAndRun(command, 'UV Add Dev Package', selectedDir);
382 |
383 | return new vscode.LanguageModelToolResult([
384 | new vscode.LanguageModelTextPart(`Successfully initiated dev package addition for "${packageName}". Check the terminal for progress and results.`)
385 | ]);
386 | } catch (error: any) {
387 | throw new Error(`Failed to add dev package: ${error.message}`);
388 | }
389 | }
390 | }
391 |
392 | export class UpgradePackagesTool extends UVToolBase {
393 | async prepareInvocation(
394 | options: vscode.LanguageModelToolInvocationPrepareOptions,
395 | _token: vscode.CancellationToken
396 | ): Promise {
397 | this.checkWorkspace();
398 | const selectedDir = await this.selectWorkingDirectory();
399 | const { upgradeType = 'all', packageName } = options.input;
400 |
401 | const description = upgradeType === 'specific' && packageName
402 | ? `Upgrade specific package: ${packageName}`
403 | : 'Upgrade all packages to latest versions';
404 |
405 | return {
406 | invocationMessage: 'Upgrading packages',
407 | confirmationMessages: {
408 | title: 'Upgrade Packages',
409 | message: new vscode.MarkdownString(
410 | `Upgrade project packages?\n\n` +
411 | `**Operation:** ${description}\n` +
412 | `**Directory:** \`${selectedDir}\`\n\n` +
413 | `This will update packages to their latest compatible versions.`
414 | )
415 | }
416 | };
417 | }
418 |
419 | async invoke(
420 | options: vscode.LanguageModelToolInvocationOptions,
421 | _token: vscode.CancellationToken
422 | ): Promise {
423 | try {
424 | this.checkWorkspace();
425 | const selectedDir = await this.selectWorkingDirectory();
426 | const { upgradeType = 'all', packageName } = options.input;
427 |
428 | if (upgradeType === 'specific' && packageName) {
429 | // For specific package upgrade, we need to check pyproject.toml
430 | const pyprojectPath = path.join(selectedDir, 'pyproject.toml');
431 | if (!fs.existsSync(pyprojectPath)) {
432 | throw new Error('pyproject.toml file not found.');
433 | }
434 |
435 | const command = `uv pip compile pyproject.toml -o uv.lock --upgrade-package ${packageName}`;
436 | await this.executeCommand(command);
437 |
438 | return new vscode.LanguageModelToolResult([
439 | new vscode.LanguageModelTextPart(`Successfully upgraded package "${packageName}" to its latest compatible version.`)
440 | ]);
441 | } else {
442 | // Upgrade all packages
443 | const command = 'uv pip install --upgrade';
444 | await this.createTerminalAndRun(command, 'UV Upgrade', selectedDir);
445 |
446 | return new vscode.LanguageModelToolResult([
447 | new vscode.LanguageModelTextPart(`Successfully initiated package upgrade for all packages. Check the terminal for progress and results.`)
448 | ]);
449 | }
450 | } catch (error: any) {
451 | throw new Error(`Failed to upgrade packages: ${error.message}`);
452 | }
453 | }
454 | }
455 |
456 | export class CleanCacheTool extends UVToolBase {
457 | async prepareInvocation(
458 | options: vscode.LanguageModelToolInvocationPrepareOptions,
459 | _token: vscode.CancellationToken
460 | ): Promise {
461 | this.checkWorkspace();
462 | // Cache cleaning doesn't require working directory selection
463 | return {
464 | invocationMessage: 'Cleaning UV cache',
465 | confirmationMessages: {
466 | title: 'Clean Cache',
467 | message: new vscode.MarkdownString(
468 | `Clean UV package cache?\n\n` +
469 | `This will remove cached packages to free up disk space and resolve potential caching issues.`
470 | )
471 | }
472 | };
473 | }
474 |
475 | async invoke(
476 | options: vscode.LanguageModelToolInvocationOptions,
477 | _token: vscode.CancellationToken
478 | ): Promise {
479 | try {
480 | const command = 'uv cache clean';
481 | await this.createTerminalAndRun(command, 'UV Clean Cache');
482 |
483 | return new vscode.LanguageModelToolResult([
484 | new vscode.LanguageModelTextPart(`Successfully initiated cache cleaning. Check the terminal for progress and results.`)
485 | ]);
486 | } catch (error: any) {
487 | throw new Error(`Failed to clean cache: ${error.message}`);
488 | }
489 | }
490 | }
491 |
492 | export class GenerateLockTool extends UVToolBase {
493 | async prepareInvocation(
494 | options: vscode.LanguageModelToolInvocationPrepareOptions,
495 | _token: vscode.CancellationToken
496 | ): Promise {
497 | this.checkWorkspace();
498 | const selectedDir = await this.selectWorkingDirectory();
499 | const { includeExtras = 'none', extras, groups } = options.input;
500 |
501 | let description = 'Generate lock file from pyproject.toml';
502 | if (includeExtras === 'all') {
503 | description += ' (including all extras)';
504 | } else if (includeExtras === 'specific' && extras) {
505 | description += ` (extras: ${extras})`;
506 | }
507 | if (groups) {
508 | description += ` (groups: ${groups})`;
509 | }
510 |
511 | return {
512 | invocationMessage: 'Generating lock file',
513 | confirmationMessages: {
514 | title: 'Generate Lock File',
515 | message: new vscode.MarkdownString(
516 | `Generate uv.lock file from pyproject.toml?\n\n` +
517 | `**Operation:** ${description}\n` +
518 | `**Directory:** \`${selectedDir}\`\n\n` +
519 | `This will create a lock file to pin exact dependency versions.`
520 | )
521 | }
522 | };
523 | }
524 |
525 | async invoke(
526 | options: vscode.LanguageModelToolInvocationOptions,
527 | _token: vscode.CancellationToken
528 | ): Promise {
529 | try {
530 | this.checkWorkspace();
531 | const selectedDir = await this.selectWorkingDirectory();
532 | const { includeExtras = 'none', extras, groups } = options.input;
533 |
534 | let command = 'uv lock';
535 |
536 | if (includeExtras === 'all') {
537 | command += ' --all-extras';
538 | } else if (includeExtras === 'specific' && extras) {
539 | const extrasList = extras.split(',').map((e: string) => e.trim());
540 | for (const extra of extrasList) {
541 | command += ` --extra ${extra}`;
542 | }
543 | }
544 |
545 | if (groups) {
546 | const groupsList = groups.split(',').map((g: string) => g.trim());
547 | for (const group of groupsList) {
548 | command += ` --group ${group}`;
549 | }
550 | }
551 |
552 | await this.createTerminalAndRun(command, 'UV Generate Lock', selectedDir);
553 |
554 | return new vscode.LanguageModelToolResult([
555 | new vscode.LanguageModelTextPart(`Successfully initiated lock file generation. Check the terminal for progress and results.`)
556 | ]);
557 | } catch (error: any) {
558 | throw new Error(`Failed to generate lock file: ${error.message}`);
559 | }
560 | }
561 | }
562 |
563 | export class CreateVenvTool extends UVToolBase {
564 | async prepareInvocation(
565 | options: vscode.LanguageModelToolInvocationPrepareOptions,
566 | _token: vscode.CancellationToken
567 | ): Promise {
568 | this.checkWorkspace();
569 | const selectedDir = await this.selectWorkingDirectory();
570 | const { pythonVersion } = options.input;
571 |
572 | let description = 'Create virtual environment';
573 | if (pythonVersion) {
574 | description += ` with Python ${pythonVersion}`;
575 | }
576 |
577 | return {
578 | invocationMessage: description,
579 | confirmationMessages: {
580 | title: 'Create Virtual Environment',
581 | message: new vscode.MarkdownString(
582 | `Create a virtual environment with UV?\n\n` +
583 | `**Directory:** \`${selectedDir}\`\n` +
584 | (pythonVersion ? `**Python Version:** \`${pythonVersion}\`\n` : '') +
585 | `\nThis will create an isolated Python environment for the project.`
586 | )
587 | }
588 | };
589 | }
590 |
591 | async invoke(
592 | options: vscode.LanguageModelToolInvocationOptions,
593 | _token: vscode.CancellationToken
594 | ): Promise {
595 | try {
596 | this.checkWorkspace();
597 | const selectedDir = await this.selectWorkingDirectory();
598 | const { pythonVersion } = options.input;
599 |
600 | let command = 'uv venv';
601 | if (pythonVersion) {
602 | command += ` --python ${pythonVersion}`;
603 | }
604 |
605 | await this.createTerminalAndRun(command, 'UV Create Venv', selectedDir);
606 |
607 | return new vscode.LanguageModelToolResult([
608 | new vscode.LanguageModelTextPart(`Successfully initiated virtual environment creation. Check the terminal for progress and results.`)
609 | ]);
610 | } catch (error: any) {
611 | throw new Error(`Failed to create virtual environment: ${error.message}`);
612 | }
613 | }
614 | }
615 |
616 | export class RunScriptTool extends UVToolBase {
617 | async prepareInvocation(
618 | options: vscode.LanguageModelToolInvocationPrepareOptions,
619 | _token: vscode.CancellationToken
620 | ): Promise {
621 | this.checkWorkspace();
622 | const selectedDir = await this.selectWorkingDirectory();
623 | const { scriptPath, pythonVersion } = options.input;
624 |
625 | return {
626 | invocationMessage: `Running script: ${scriptPath}`,
627 | confirmationMessages: {
628 | title: 'Run Python Script',
629 | message: new vscode.MarkdownString(
630 | `Run Python script with UV?\n\n` +
631 | `**Script:** \`${scriptPath}\`\n` +
632 | `**Directory:** \`${selectedDir}\`\n` +
633 | (pythonVersion ? `**Python Version:** \`${pythonVersion}\`\n` : '') +
634 | `\nThis will execute the script using UV's managed environment.`
635 | )
636 | }
637 | };
638 | }
639 |
640 | async invoke(
641 | options: vscode.LanguageModelToolInvocationOptions,
642 | _token: vscode.CancellationToken
643 | ): Promise {
644 | try {
645 | this.checkWorkspace();
646 | const selectedDir = await this.selectWorkingDirectory();
647 | const { scriptPath, pythonVersion } = options.input;
648 |
649 | let command = `uv run ${scriptPath}`;
650 | if (pythonVersion) {
651 | command = `uv run --python ${pythonVersion} ${scriptPath}`;
652 | }
653 |
654 | await this.createTerminalAndRun(command, 'UV Run Script', selectedDir);
655 |
656 | return new vscode.LanguageModelToolResult([
657 | new vscode.LanguageModelTextPart(`Successfully initiated script execution for "${scriptPath}". Check the terminal for progress and results.`)
658 | ]);
659 | } catch (error: any) {
660 | throw new Error(`Failed to run script: ${error.message}`);
661 | }
662 | }
663 | }
664 |
665 | export class AddScriptDependencyTool extends UVToolBase {
666 | async prepareInvocation(
667 | options: vscode.LanguageModelToolInvocationPrepareOptions,
668 | _token: vscode.CancellationToken
669 | ): Promise {
670 | this.checkWorkspace();
671 | const selectedDir = await this.selectWorkingDirectory();
672 | const { scriptPath, packageName } = options.input;
673 |
674 | return {
675 | invocationMessage: `Adding dependency ${packageName} to ${scriptPath}`,
676 | confirmationMessages: {
677 | title: 'Add Script Dependency',
678 | message: new vscode.MarkdownString(
679 | `Add inline dependency to Python script?\n\n` +
680 | `**Script:** \`${scriptPath}\`\n` +
681 | `**Package:** \`${packageName}\`\n` +
682 | `**Directory:** \`${selectedDir}\`\n\n` +
683 | `This will add the package as an inline dependency to the script.`
684 | )
685 | }
686 | };
687 | }
688 |
689 | async invoke(
690 | options: vscode.LanguageModelToolInvocationOptions,
691 | _token: vscode.CancellationToken
692 | ): Promise {
693 | try {
694 | this.checkWorkspace();
695 | const selectedDir = await this.selectWorkingDirectory();
696 | const { scriptPath, packageName } = options.input;
697 |
698 | const command = `uv add --script ${scriptPath} ${packageName}`;
699 |
700 | await this.createTerminalAndRun(command, 'UV Add Script Dependency', selectedDir);
701 |
702 | return new vscode.LanguageModelToolResult([
703 | new vscode.LanguageModelTextPart(`Successfully initiated adding dependency "${packageName}" to script "${scriptPath}". Check the terminal for progress and results.`)
704 | ]);
705 | } catch (error: any) {
706 | throw new Error(`Failed to add script dependency: ${error.message}`);
707 | }
708 | }
709 | }
710 |
711 | export class InstallPythonTool extends UVToolBase {
712 | async prepareInvocation(
713 | options: vscode.LanguageModelToolInvocationPrepareOptions,
714 | _token: vscode.CancellationToken
715 | ): Promise {
716 | const { pythonVersion } = options.input;
717 |
718 | return {
719 | invocationMessage: `Installing Python ${pythonVersion}`,
720 | confirmationMessages: {
721 | title: 'Install Python Version',
722 | message: new vscode.MarkdownString(
723 | `Install Python version with UV?\n\n` +
724 | `**Python Version:** \`${pythonVersion}\`\n\n` +
725 | `This will install the specified Python version using UV's Python management.`
726 | )
727 | }
728 | };
729 | }
730 |
731 | async invoke(
732 | options: vscode.LanguageModelToolInvocationOptions,
733 | _token: vscode.CancellationToken
734 | ): Promise {
735 | try {
736 | const { pythonVersion } = options.input;
737 |
738 | const command = `uv python install ${pythonVersion}`;
739 |
740 | await this.createTerminalAndRun(command, 'UV Install Python');
741 |
742 | return new vscode.LanguageModelToolResult([
743 | new vscode.LanguageModelTextPart(`Successfully initiated Python ${pythonVersion} installation. Check the terminal for progress and results.`)
744 | ]);
745 | } catch (error: any) {
746 | throw new Error(`Failed to install Python: ${error.message}`);
747 | }
748 | }
749 | }
750 |
751 | export class PinPythonTool extends UVToolBase {
752 | async prepareInvocation(
753 | options: vscode.LanguageModelToolInvocationPrepareOptions,
754 | _token: vscode.CancellationToken
755 | ): Promise {
756 | this.checkWorkspace();
757 | const selectedDir = await this.selectWorkingDirectory();
758 | const { pythonVersion } = options.input;
759 |
760 | return {
761 | invocationMessage: `Pinning Python ${pythonVersion}`,
762 | confirmationMessages: {
763 | title: 'Pin Python Version',
764 | message: new vscode.MarkdownString(
765 | `Pin Python version for the project?\n\n` +
766 | `**Python Version:** \`${pythonVersion}\`\n` +
767 | `**Directory:** \`${selectedDir}\`\n\n` +
768 | `This will set the Python version for the current project.`
769 | )
770 | }
771 | };
772 | }
773 |
774 | async invoke(
775 | options: vscode.LanguageModelToolInvocationOptions,
776 | _token: vscode.CancellationToken
777 | ): Promise {
778 | try {
779 | this.checkWorkspace();
780 | const selectedDir = await this.selectWorkingDirectory();
781 | const { pythonVersion } = options.input;
782 |
783 | const command = `uv python pin ${pythonVersion}`;
784 |
785 | await this.createTerminalAndRun(command, 'UV Pin Python', selectedDir);
786 |
787 | return new vscode.LanguageModelToolResult([
788 | new vscode.LanguageModelTextPart(`Successfully initiated Python ${pythonVersion} pinning for the project. Check the terminal for progress and results.`)
789 | ]);
790 | } catch (error: any) {
791 | throw new Error(`Failed to pin Python: ${error.message}`);
792 | }
793 | }
794 | }
795 |
796 | export class InstallToolTool extends UVToolBase {
797 | async prepareInvocation(
798 | options: vscode.LanguageModelToolInvocationPrepareOptions,
799 | _token: vscode.CancellationToken
800 | ): Promise {
801 | const { toolName } = options.input;
802 |
803 | return {
804 | invocationMessage: `Installing tool: ${toolName}`,
805 | confirmationMessages: {
806 | title: 'Install Tool',
807 | message: new vscode.MarkdownString(
808 | `Install Python tool globally with UV?\n\n` +
809 | `**Tool:** \`${toolName}\`\n\n` +
810 | `This will install the tool globally using UV's tool management.`
811 | )
812 | }
813 | };
814 | }
815 |
816 | async invoke(
817 | options: vscode.LanguageModelToolInvocationOptions,
818 | _token: vscode.CancellationToken
819 | ): Promise {
820 | try {
821 | const { toolName } = options.input;
822 |
823 | const command = `uv tool install ${toolName}`;
824 |
825 | await this.createTerminalAndRun(command, 'UV Install Tool');
826 |
827 | return new vscode.LanguageModelToolResult([
828 | new vscode.LanguageModelTextPart(`Successfully initiated tool installation for "${toolName}". Check the terminal for progress and results.`)
829 | ]);
830 | } catch (error: any) {
831 | throw new Error(`Failed to install tool: ${error.message}`);
832 | }
833 | }
834 | }
835 |
836 | export class ActivateVirtualEnvTool extends UVToolBase {
837 | async prepareInvocation(
838 | options: vscode.LanguageModelToolInvocationPrepareOptions,
839 | _token: vscode.CancellationToken
840 | ): Promise {
841 | this.checkWorkspace();
842 | const selectedDir = await this.selectWorkingDirectory();
843 |
844 | return {
845 | invocationMessage: 'Activating virtual environment',
846 | confirmationMessages: {
847 | title: 'Activate Virtual Environment',
848 | message: new vscode.MarkdownString(
849 | `Activate a virtual environment?\n\n` +
850 | `**Directory:** \`${selectedDir}\`\n\n` +
851 | `This will search for existing virtual environments and activate one in a new terminal. If no virtual environment is found, you'll have the option to create one.`
852 | )
853 | }
854 | };
855 | }
856 |
857 | async invoke(
858 | options: vscode.LanguageModelToolInvocationOptions,
859 | _token: vscode.CancellationToken
860 | ): Promise {
861 | try {
862 | this.checkWorkspace();
863 | const selectedDir = await this.selectWorkingDirectory();
864 |
865 | // Common virtual environment paths to check
866 | const commonVenvPaths = [
867 | path.join(selectedDir, '.venv'),
868 | path.join(selectedDir, 'venv'),
869 | path.join(selectedDir, '.env'),
870 | path.join(selectedDir, 'env')
871 | ];
872 |
873 | // Find existing virtual environments
874 | const existingVenvs: string[] = [];
875 | for (const venvPath of commonVenvPaths) {
876 | if (fs.existsSync(venvPath)) {
877 | // Check if it's a valid virtual environment
878 | const activateScript = process.platform === 'win32'
879 | ? path.join(venvPath, 'Scripts', 'activate.bat')
880 | : path.join(venvPath, 'bin', 'activate');
881 |
882 | if (fs.existsSync(activateScript)) {
883 | existingVenvs.push(venvPath);
884 | }
885 | }
886 | }
887 |
888 | let selectedVenvPath: string;
889 |
890 | if (existingVenvs.length === 0) {
891 | return new vscode.LanguageModelToolResult([
892 | new vscode.LanguageModelTextPart(`No virtual environments found in ${selectedDir}. You can create one using the "UV: Create Virtual Environment" command or the create_venv tool.`)
893 | ]);
894 | } else if (existingVenvs.length === 1) {
895 | // Only one virtual environment found, use it
896 | selectedVenvPath = existingVenvs[0];
897 | } else {
898 | // Multiple virtual environments found, use the first one (.venv is preferred)
899 | selectedVenvPath = existingVenvs[0];
900 | }
901 |
902 | // Determine the activation command based on the platform
903 | let activationCommand: string;
904 | if (process.platform === 'win32') {
905 | // Windows
906 | const activateScript = path.join(selectedVenvPath, 'Scripts', 'activate.bat');
907 | activationCommand = `"${activateScript}"`;
908 | } else {
909 | // Unix-like systems (macOS, Linux)
910 | const activateScript = path.join(selectedVenvPath, 'bin', 'activate');
911 | activationCommand = `source "${activateScript}"`;
912 | }
913 |
914 | // Create a new terminal and activate the virtual environment
915 | const terminal = vscode.window.createTerminal({
916 | name: 'UV Virtual Environment',
917 | cwd: selectedDir
918 | });
919 |
920 | terminal.show();
921 | terminal.sendText(activationCommand);
922 |
923 | const venvName = path.basename(selectedVenvPath);
924 | return new vscode.LanguageModelToolResult([
925 | new vscode.LanguageModelTextPart(`Successfully activated virtual environment "${venvName}" in a new terminal. The virtual environment is now active and ready to use.`)
926 | ]);
927 | } catch (error: any) {
928 | throw new Error(`Failed to activate virtual environment: ${error.message}`);
929 | }
930 | }
931 | }
932 |
933 | export class RunToolTool extends UVToolBase {
934 | async prepareInvocation(
935 | options: vscode.LanguageModelToolInvocationPrepareOptions,
936 | _token: vscode.CancellationToken
937 | ): Promise {
938 | const { toolName, arguments: args } = options.input;
939 |
940 | let description = `Run tool: ${toolName}`;
941 | if (args) {
942 | description += ` with arguments: ${args}`;
943 | }
944 |
945 | return {
946 | invocationMessage: description,
947 | confirmationMessages: {
948 | title: 'Run Tool with UVX',
949 | message: new vscode.MarkdownString(
950 | `Run Python tool with UVX?\n\n` +
951 | `**Tool:** \`${toolName}\`\n` +
952 | (args ? `**Arguments:** \`${args}\`\n` : '') +
953 | `\nThis will execute the tool using UVX (UV's tool runner).`
954 | )
955 | }
956 | };
957 | }
958 |
959 | async invoke(
960 | options: vscode.LanguageModelToolInvocationOptions,
961 | _token: vscode.CancellationToken
962 | ): Promise {
963 | try {
964 | const { toolName, arguments: args } = options.input;
965 |
966 | let command = `uvx ${toolName}`;
967 | if (args) {
968 | command += ` ${args}`;
969 | }
970 |
971 | await this.createTerminalAndRun(command, 'UVX Run Tool');
972 |
973 | return new vscode.LanguageModelToolResult([
974 | new vscode.LanguageModelTextPart(`Successfully initiated tool execution for "${toolName}". Check the terminal for progress and results.`)
975 | ]);
976 | } catch (error: any) {
977 | throw new Error(`Failed to run tool: ${error.message}`);
978 | }
979 | }
980 | }
981 |
982 | // Registration function
983 | export function registerLanguageModelTools(context: vscode.ExtensionContext) {
984 | try {
985 | context.subscriptions.push(vscode.lm.registerTool('init_project', new InitProjectTool()));
986 | context.subscriptions.push(vscode.lm.registerTool('sync_dependencies', new SyncDependenciesTool()));
987 | context.subscriptions.push(vscode.lm.registerTool('add_package', new AddPackageTool()));
988 | context.subscriptions.push(vscode.lm.registerTool('add_dev_package', new AddDevPackageTool()));
989 | context.subscriptions.push(vscode.lm.registerTool('upgrade_packages', new UpgradePackagesTool()));
990 | context.subscriptions.push(vscode.lm.registerTool('clean_cache', new CleanCacheTool()));
991 | context.subscriptions.push(vscode.lm.registerTool('generate_lock', new GenerateLockTool()));
992 | context.subscriptions.push(vscode.lm.registerTool('create_venv', new CreateVenvTool()));
993 | context.subscriptions.push(vscode.lm.registerTool('run_script', new RunScriptTool()));
994 | context.subscriptions.push(vscode.lm.registerTool('add_script_dependency', new AddScriptDependencyTool()));
995 | context.subscriptions.push(vscode.lm.registerTool('install_python', new InstallPythonTool()));
996 | context.subscriptions.push(vscode.lm.registerTool('pin_python', new PinPythonTool()));
997 | context.subscriptions.push(vscode.lm.registerTool('install_tool', new InstallToolTool()));
998 | context.subscriptions.push(vscode.lm.registerTool('run_tool', new RunToolTool()));
999 | context.subscriptions.push(vscode.lm.registerTool('activate_venv', new ActivateVirtualEnvTool()));
1000 | } catch (error) {
1001 | console.warn('Failed to register some language model tools:', error);
1002 | }
1003 | }
1004 |
--------------------------------------------------------------------------------