|
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