|
1 |
| -import * as vscode from 'vscode'; |
2 |
| -import * as path from 'path'; |
3 |
| -import * as fs from 'fs'; |
4 |
| - |
5 |
| -type TreeItemType = FolderItem | ModuleItem | TaskItem; |
6 |
| - |
7 |
| -class FolderItem extends vscode.TreeItem { |
8 |
| - constructor( |
9 |
| - public readonly label: string, |
10 |
| - public readonly children: (FolderItem | ModuleItem)[] = [], |
11 |
| - public readonly folderPath: string |
12 |
| - ) { |
13 |
| - super(label, vscode.TreeItemCollapsibleState.Expanded); |
14 |
| - this.contextValue = 'folder'; |
15 |
| - this.iconPath = new vscode.ThemeIcon('folder'); |
16 |
| - this.tooltip = folderPath; |
17 |
| - } |
18 |
| -} |
19 |
| - |
20 |
| -class ModuleItem extends vscode.TreeItem { |
21 |
| - constructor( |
22 |
| - public readonly label: string, |
23 |
| - public readonly children: TaskItem[] = [], |
24 |
| - public readonly filePath: string |
25 |
| - ) { |
26 |
| - super(label, vscode.TreeItemCollapsibleState.Collapsed); |
27 |
| - this.contextValue = 'module'; |
28 |
| - this.iconPath = new vscode.ThemeIcon('symbol-file'); |
29 |
| - this.tooltip = filePath; |
30 |
| - this.command = { |
31 |
| - command: 'vscode.open', |
32 |
| - title: 'Open Module File', |
33 |
| - arguments: [vscode.Uri.file(filePath)], |
34 |
| - }; |
35 |
| - } |
36 |
| -} |
37 |
| - |
38 |
| -class TaskItem extends vscode.TreeItem { |
39 |
| - constructor( |
40 |
| - public readonly label: string, |
41 |
| - public readonly filePath: string, |
42 |
| - public readonly lineNumber: number |
43 |
| - ) { |
44 |
| - super(label, vscode.TreeItemCollapsibleState.None); |
45 |
| - this.contextValue = 'task'; |
46 |
| - this.iconPath = new vscode.ThemeIcon('symbol-method'); |
47 |
| - this.tooltip = `${this.label} - ${path.basename(this.filePath)}:${this.lineNumber}`; |
48 |
| - this.description = path.basename(this.filePath); |
49 |
| - this.command = { |
50 |
| - command: 'vscode.open', |
51 |
| - title: 'Open Task File', |
52 |
| - arguments: [ |
53 |
| - vscode.Uri.file(this.filePath), |
54 |
| - { |
55 |
| - selection: new vscode.Range( |
56 |
| - new vscode.Position(this.lineNumber - 1, 0), |
57 |
| - new vscode.Position(this.lineNumber - 1, 0) |
58 |
| - ), |
59 |
| - }, |
60 |
| - ], |
61 |
| - }; |
62 |
| - } |
63 |
| -} |
64 |
| - |
65 |
| -export class PyTaskProvider implements vscode.TreeDataProvider<TreeItemType> { |
66 |
| - private _onDidChangeTreeData: vscode.EventEmitter<TreeItemType | undefined | null | void> = |
67 |
| - new vscode.EventEmitter<TreeItemType | undefined | null | void>(); |
68 |
| - readonly onDidChangeTreeData: vscode.Event<TreeItemType | undefined | null | void> = |
69 |
| - this._onDidChangeTreeData.event; |
70 |
| - private fileSystemWatcher: vscode.FileSystemWatcher; |
71 |
| - |
72 |
| - constructor() { |
73 |
| - this.fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/task_*.py'); |
74 |
| - |
75 |
| - this.fileSystemWatcher.onDidCreate(() => { |
76 |
| - this.refresh(); |
77 |
| - }); |
78 |
| - |
79 |
| - this.fileSystemWatcher.onDidChange(() => { |
80 |
| - this.refresh(); |
81 |
| - }); |
82 |
| - |
83 |
| - this.fileSystemWatcher.onDidDelete(() => { |
84 |
| - this.refresh(); |
85 |
| - }); |
86 |
| - } |
87 |
| - |
88 |
| - dispose() { |
89 |
| - this.fileSystemWatcher.dispose(); |
90 |
| - } |
91 |
| - |
92 |
| - refresh(): void { |
93 |
| - this._onDidChangeTreeData.fire(); |
94 |
| - } |
95 |
| - |
96 |
| - getTreeItem(element: TreeItemType): vscode.TreeItem { |
97 |
| - return element; |
98 |
| - } |
99 |
| - |
100 |
| - async getChildren(element?: TreeItemType): Promise<TreeItemType[]> { |
101 |
| - if (!element) { |
102 |
| - return this.buildFileTree(); |
103 |
| - } |
104 |
| - |
105 |
| - if (element instanceof FolderItem) { |
106 |
| - return element.children; |
107 |
| - } |
108 |
| - |
109 |
| - if (element instanceof ModuleItem) { |
110 |
| - return element.children; |
111 |
| - } |
112 |
| - |
113 |
| - return []; |
114 |
| - } |
115 |
| - |
116 |
| - private async buildFileTree(): Promise<TreeItemType[]> { |
117 |
| - const workspaceFolders = vscode.workspace.workspaceFolders; |
118 |
| - if (!workspaceFolders) { |
119 |
| - return []; |
120 |
| - } |
121 |
| - |
122 |
| - const rootItems = new Map<string, TreeItemType>(); |
123 |
| - |
124 |
| - // Get all task modules across the workspace |
125 |
| - const taskFiles = await vscode.workspace.findFiles( |
126 |
| - '**/task_*.py', |
127 |
| - '{**/node_modules/**,**/.venv/**,**/.git/**,**/.pixi/**,**/venv/**,**/__pycache__/**}' |
128 |
| - ); |
129 |
| - |
130 |
| - // Process each task module |
131 |
| - for (const taskFile of taskFiles) { |
132 |
| - const relativePath = path.relative(workspaceFolders[0].uri.fsPath, taskFile.fsPath); |
133 |
| - const dirPath = path.dirname(relativePath); |
134 |
| - const fileName = path.basename(taskFile.fsPath); |
135 |
| - |
136 |
| - // Create folder hierarchy |
137 |
| - let currentPath = ''; |
138 |
| - let currentItems = rootItems; |
139 |
| - const pathParts = dirPath.split(path.sep); |
140 |
| - |
141 |
| - // Skip if it's in the root |
142 |
| - if (dirPath !== '.') { |
143 |
| - for (const part of pathParts) { |
144 |
| - currentPath = currentPath ? path.join(currentPath, part) : part; |
145 |
| - const fullPath = path.join(workspaceFolders[0].uri.fsPath, currentPath); |
146 |
| - |
147 |
| - if (!currentItems.has(currentPath)) { |
148 |
| - const newFolder = new FolderItem(part, [], fullPath); |
149 |
| - currentItems.set(currentPath, newFolder); |
150 |
| - } |
151 |
| - |
152 |
| - const folderItem = currentItems.get(currentPath); |
153 |
| - if (folderItem instanceof FolderItem) { |
154 |
| - currentItems = new Map( |
155 |
| - folderItem.children |
156 |
| - .filter((child) => child instanceof FolderItem) |
157 |
| - .map((child) => [path.basename(child.label), child as FolderItem]) |
158 |
| - ); |
159 |
| - } |
160 |
| - } |
161 |
| - } |
162 |
| - |
163 |
| - // Create module and its tasks |
164 |
| - const content = fs.readFileSync(taskFile.fsPath, 'utf8'); |
165 |
| - const taskItems = this.findTaskFunctions(taskFile.fsPath, content); |
166 |
| - const moduleItem = new ModuleItem(fileName, taskItems, taskFile.fsPath); |
167 |
| - |
168 |
| - // Add module to appropriate folder or root |
169 |
| - if (dirPath === '.') { |
170 |
| - rootItems.set(fileName, moduleItem); |
171 |
| - } else { |
172 |
| - const parentFolder = rootItems.get(dirPath); |
173 |
| - if (parentFolder instanceof FolderItem) { |
174 |
| - parentFolder.children.push(moduleItem); |
175 |
| - } |
176 |
| - } |
177 |
| - } |
178 |
| - |
179 |
| - // Sort everything |
180 |
| - const result = Array.from(rootItems.values()); |
181 |
| - |
182 |
| - // Sort folders and modules |
183 |
| - result.sort((a, b) => { |
184 |
| - // Folders come before modules |
185 |
| - if (a instanceof FolderItem && !(b instanceof FolderItem)) return -1; |
186 |
| - if (!(a instanceof FolderItem) && b instanceof FolderItem) return 1; |
187 |
| - // Alphabetical sort within same type |
188 |
| - return a.label.localeCompare(b.label); |
189 |
| - }); |
190 |
| - |
191 |
| - return result; |
192 |
| - } |
193 |
| - |
194 |
| - findTaskFunctions(filePath: string, content: string): TaskItem[] { |
195 |
| - // Find out whether the task decorator is used in the file. |
196 |
| - |
197 |
| - // Booleans to track if the task decorator is imported as `from pytask import task` |
198 |
| - // and used as `@task` or `import pytask` and used as `@pytask.task`. |
199 |
| - let hasTaskImport = false; |
200 |
| - let taskAlias = 'task'; // default name for 'from pytask import task' |
201 |
| - let pytaskAlias = 'pytask'; // default name for 'import pytask' |
202 |
| - let hasPytaskImport = false; |
203 |
| - |
204 |
| - // Match the import statements |
205 |
| - // Handle various import patterns: |
206 |
| - // - from pytask import task |
207 |
| - // - from pytask import task as t |
208 |
| - // - from pytask import Product, task |
209 |
| - // - from pytask import (Product, task) |
210 |
| - const fromPytaskImport = content.match( |
211 |
| - /from\s+pytask\s+import\s+(?:\(?\s*(?:[\w]+\s*,\s*)*task(?:\s+as\s+(\w+))?(?:\s*,\s*[\w]+)*\s*\)?)/ |
212 |
| - ); |
213 |
| - const importPytask = content.match(/import\s+pytask(?:\s+as\s+(\w+))?\s*$/m); |
214 |
| - |
215 |
| - if (fromPytaskImport) { |
216 |
| - hasTaskImport = true; |
217 |
| - if (fromPytaskImport[1]) { |
218 |
| - taskAlias = fromPytaskImport[1]; |
219 |
| - } |
220 |
| - } |
221 |
| - |
222 |
| - if (importPytask) { |
223 |
| - hasPytaskImport = true; |
224 |
| - // If there's an alias (import pytask as something), use it |
225 |
| - pytaskAlias = importPytask[1] || 'pytask'; |
226 |
| - } |
227 |
| - |
228 |
| - // Find the tasks. |
229 |
| - const tasks: TaskItem[] = []; |
230 |
| - const lines = content.split('\n'); |
231 |
| - |
232 |
| - let isDecorated = false; |
233 |
| - for (let i = 0; i < lines.length; i++) { |
234 |
| - const line = lines[i].trim(); |
235 |
| - |
236 |
| - // Check for decorators |
237 |
| - if (line.startsWith('@')) { |
238 |
| - // Handle both @task and @pytask.task(...) patterns |
239 |
| - isDecorated = |
240 |
| - (hasTaskImport && line === `@${taskAlias}`) || |
241 |
| - (hasPytaskImport && line.startsWith(`@${pytaskAlias}.task`)); |
242 |
| - continue; |
243 |
| - } |
244 |
| - |
245 |
| - // Check for function definitions |
246 |
| - const funcMatch = line.match(/^def\s+(\w+)\s*\(/); |
247 |
| - if (funcMatch) { |
248 |
| - const funcName = funcMatch[1]; |
249 |
| - // Add if it's a task_* function or has a task decorator |
250 |
| - if (funcName.startsWith('task_') || isDecorated) { |
251 |
| - tasks.push(new TaskItem(funcName, filePath, i + 1)); |
252 |
| - } |
253 |
| - isDecorated = false; // Reset decorator flag |
254 |
| - } |
255 |
| - } |
256 |
| - |
257 |
| - // Sort the tasks by name. |
258 |
| - tasks.sort((a, b) => a.label.localeCompare(b.label)); |
259 |
| - |
260 |
| - return tasks; |
261 |
| - } |
262 |
| -} |
| 1 | +import * as vscode from "vscode"; |
| 2 | +import { activate as activateTaskProvider } from "./providers/taskProvider"; |
263 | 3 |
|
264 | 4 | export function activate(context: vscode.ExtensionContext) {
|
265 |
| - const pytaskProvider = new PyTaskProvider(); |
266 |
| - const treeView = vscode.window.createTreeView('pytaskExplorer', { |
267 |
| - treeDataProvider: pytaskProvider, |
268 |
| - showCollapseAll: true, |
269 |
| - }); |
270 |
| - |
271 |
| - context.subscriptions.push(treeView); |
272 |
| - |
273 |
| - const refreshCommand = vscode.commands.registerCommand('pytask.refresh', () => { |
274 |
| - pytaskProvider.refresh(); |
275 |
| - }); |
276 |
| - |
277 |
| - context.subscriptions.push(refreshCommand, pytaskProvider); |
| 5 | + activateTaskProvider(context); |
278 | 6 | }
|
279 | 7 |
|
280 | 8 | export function deactivate() {}
|
0 commit comments