Skip to content

Restructure extension. #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ repos:
files: \.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx
types: [file]
additional_dependencies:
- eslint
- typescript
- '@typescript-eslint/parser'
- '@typescript-eslint/eslint-plugin'
- eslint@8.56.0
- '@typescript-eslint/parser@6.18.1'
- '@typescript-eslint/eslint-plugin@6.18.1'
- '@eslint/js@8.56.0'
- repo: https://github.com/lyz-code/yamlfix
rev: 1.17.0
hooks:
Expand Down
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"printWidth": 100,
"singleQuote": false
}
40 changes: 20 additions & 20 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import js from '@eslint/js';
import typescript from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import js from "@eslint/js";
import typescript from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";

export default [
js.configs.recommended,
{
files: ['**/*.ts'],
files: ["**/*.ts"],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaVersion: "latest",
sourceType: "module",
},
globals: {
console: 'readonly',
process: 'readonly',
__dirname: 'readonly',
setTimeout: 'readonly',
console: "readonly",
process: "readonly",
__dirname: "readonly",
setTimeout: "readonly",
},
},
plugins: {
'@typescript-eslint': typescript,
"@typescript-eslint": typescript,
},
rules: {
...typescript.configs['recommended'].rules,
...typescript.configs["recommended"].rules,
},
},
{
files: ['**/test/**/*.ts'],
files: ["**/test/**/*.ts"],
languageOptions: {
globals: {
suite: 'readonly',
test: 'readonly',
teardown: 'readonly',
suiteSetup: 'readonly',
suiteTeardown: 'readonly',
setup: 'readonly',
suite: "readonly",
test: "readonly",
teardown: "readonly",
suiteSetup: "readonly",
suiteTeardown: "readonly",
setup: "readonly",
},
},
rules: {
'@typescript-eslint/no-unused-vars': 'off',
"@typescript-eslint/no-unused-vars": "off",
},
},
];
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint && npm run format",
"pretest": "rm -rf out && npm run compile && npm run lint && npm run format",
"lint": "eslint src",
"test": "node ./out/test/runTest.js",
"format": "prettier --write \"src/**/*.{ts,js,json,md}\""
Expand Down
278 changes: 3 additions & 275 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,280 +1,8 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';

type TreeItemType = FolderItem | ModuleItem | TaskItem;

class FolderItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly children: (FolderItem | ModuleItem)[] = [],
public readonly folderPath: string
) {
super(label, vscode.TreeItemCollapsibleState.Expanded);
this.contextValue = 'folder';
this.iconPath = new vscode.ThemeIcon('folder');
this.tooltip = folderPath;
}
}

class ModuleItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly children: TaskItem[] = [],
public readonly filePath: string
) {
super(label, vscode.TreeItemCollapsibleState.Collapsed);
this.contextValue = 'module';
this.iconPath = new vscode.ThemeIcon('symbol-file');
this.tooltip = filePath;
this.command = {
command: 'vscode.open',
title: 'Open Module File',
arguments: [vscode.Uri.file(filePath)],
};
}
}

class TaskItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly filePath: string,
public readonly lineNumber: number
) {
super(label, vscode.TreeItemCollapsibleState.None);
this.contextValue = 'task';
this.iconPath = new vscode.ThemeIcon('symbol-method');
this.tooltip = `${this.label} - ${path.basename(this.filePath)}:${this.lineNumber}`;
this.description = path.basename(this.filePath);
this.command = {
command: 'vscode.open',
title: 'Open Task File',
arguments: [
vscode.Uri.file(this.filePath),
{
selection: new vscode.Range(
new vscode.Position(this.lineNumber - 1, 0),
new vscode.Position(this.lineNumber - 1, 0)
),
},
],
};
}
}

export class PyTaskProvider implements vscode.TreeDataProvider<TreeItemType> {
private _onDidChangeTreeData: vscode.EventEmitter<TreeItemType | undefined | null | void> =
new vscode.EventEmitter<TreeItemType | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<TreeItemType | undefined | null | void> =
this._onDidChangeTreeData.event;
private fileSystemWatcher: vscode.FileSystemWatcher;

constructor() {
this.fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/task_*.py');

this.fileSystemWatcher.onDidCreate(() => {
this.refresh();
});

this.fileSystemWatcher.onDidChange(() => {
this.refresh();
});

this.fileSystemWatcher.onDidDelete(() => {
this.refresh();
});
}

dispose() {
this.fileSystemWatcher.dispose();
}

refresh(): void {
this._onDidChangeTreeData.fire();
}

getTreeItem(element: TreeItemType): vscode.TreeItem {
return element;
}

async getChildren(element?: TreeItemType): Promise<TreeItemType[]> {
if (!element) {
return this.buildFileTree();
}

if (element instanceof FolderItem) {
return element.children;
}

if (element instanceof ModuleItem) {
return element.children;
}

return [];
}

private async buildFileTree(): Promise<TreeItemType[]> {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) {
return [];
}

const rootItems = new Map<string, TreeItemType>();

// Get all task modules across the workspace
const taskFiles = await vscode.workspace.findFiles(
'**/task_*.py',
'{**/node_modules/**,**/.venv/**,**/.git/**,**/.pixi/**,**/venv/**,**/__pycache__/**}'
);

// Process each task module
for (const taskFile of taskFiles) {
const relativePath = path.relative(workspaceFolders[0].uri.fsPath, taskFile.fsPath);
const dirPath = path.dirname(relativePath);
const fileName = path.basename(taskFile.fsPath);

// Create folder hierarchy
let currentPath = '';
let currentItems = rootItems;
const pathParts = dirPath.split(path.sep);

// Skip if it's in the root
if (dirPath !== '.') {
for (const part of pathParts) {
currentPath = currentPath ? path.join(currentPath, part) : part;
const fullPath = path.join(workspaceFolders[0].uri.fsPath, currentPath);

if (!currentItems.has(currentPath)) {
const newFolder = new FolderItem(part, [], fullPath);
currentItems.set(currentPath, newFolder);
}

const folderItem = currentItems.get(currentPath);
if (folderItem instanceof FolderItem) {
currentItems = new Map(
folderItem.children
.filter((child) => child instanceof FolderItem)
.map((child) => [path.basename(child.label), child as FolderItem])
);
}
}
}

// Create module and its tasks
const content = fs.readFileSync(taskFile.fsPath, 'utf8');
const taskItems = this.findTaskFunctions(taskFile.fsPath, content);
const moduleItem = new ModuleItem(fileName, taskItems, taskFile.fsPath);

// Add module to appropriate folder or root
if (dirPath === '.') {
rootItems.set(fileName, moduleItem);
} else {
const parentFolder = rootItems.get(dirPath);
if (parentFolder instanceof FolderItem) {
parentFolder.children.push(moduleItem);
}
}
}

// Sort everything
const result = Array.from(rootItems.values());

// Sort folders and modules
result.sort((a, b) => {
// Folders come before modules
if (a instanceof FolderItem && !(b instanceof FolderItem)) return -1;
if (!(a instanceof FolderItem) && b instanceof FolderItem) return 1;
// Alphabetical sort within same type
return a.label.localeCompare(b.label);
});

return result;
}

findTaskFunctions(filePath: string, content: string): TaskItem[] {
// Find out whether the task decorator is used in the file.

// Booleans to track if the task decorator is imported as `from pytask import task`
// and used as `@task` or `import pytask` and used as `@pytask.task`.
let hasTaskImport = false;
let taskAlias = 'task'; // default name for 'from pytask import task'
let pytaskAlias = 'pytask'; // default name for 'import pytask'
let hasPytaskImport = false;

// Match the import statements
// Handle various import patterns:
// - from pytask import task
// - from pytask import task as t
// - from pytask import Product, task
// - from pytask import (Product, task)
const fromPytaskImport = content.match(
/from\s+pytask\s+import\s+(?:\(?\s*(?:[\w]+\s*,\s*)*task(?:\s+as\s+(\w+))?(?:\s*,\s*[\w]+)*\s*\)?)/
);
const importPytask = content.match(/import\s+pytask(?:\s+as\s+(\w+))?\s*$/m);

if (fromPytaskImport) {
hasTaskImport = true;
if (fromPytaskImport[1]) {
taskAlias = fromPytaskImport[1];
}
}

if (importPytask) {
hasPytaskImport = true;
// If there's an alias (import pytask as something), use it
pytaskAlias = importPytask[1] || 'pytask';
}

// Find the tasks.
const tasks: TaskItem[] = [];
const lines = content.split('\n');

let isDecorated = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();

// Check for decorators
if (line.startsWith('@')) {
// Handle both @task and @pytask.task(...) patterns
isDecorated =
(hasTaskImport && line === `@${taskAlias}`) ||
(hasPytaskImport && line.startsWith(`@${pytaskAlias}.task`));
continue;
}

// Check for function definitions
const funcMatch = line.match(/^def\s+(\w+)\s*\(/);
if (funcMatch) {
const funcName = funcMatch[1];
// Add if it's a task_* function or has a task decorator
if (funcName.startsWith('task_') || isDecorated) {
tasks.push(new TaskItem(funcName, filePath, i + 1));
}
isDecorated = false; // Reset decorator flag
}
}

// Sort the tasks by name.
tasks.sort((a, b) => a.label.localeCompare(b.label));

return tasks;
}
}
import * as vscode from "vscode";
import { activate as activateTaskProvider } from "./providers/taskProvider";

export function activate(context: vscode.ExtensionContext) {
const pytaskProvider = new PyTaskProvider();
const treeView = vscode.window.createTreeView('pytaskExplorer', {
treeDataProvider: pytaskProvider,
showCollapseAll: true,
});

context.subscriptions.push(treeView);

const refreshCommand = vscode.commands.registerCommand('pytask.refresh', () => {
pytaskProvider.refresh();
});

context.subscriptions.push(refreshCommand, pytaskProvider);
activateTaskProvider(context);
}

export function deactivate() {}
Loading
Loading