Skip to content
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Change Log

## [0.2.1] - 2026-04-27

### Fixed

- Remote SSH/provider compatibility for copy/create structure by keeping filesystem operations Uri-native end-to-end (no early `fsPath` conversion).
- Plain Text multi-root parsing for service-style trees and root-level file entries.
- Plain Text validation handling for decorative separator lines between root blocks.
- Tree formatter root-level connector behavior (`├──` / `└──`) and spacing between root sections.
- JSON structure support for `null` file leaves (e.g. `"Dockerfile": null`) in validation, preview, and creation.

## [0.2.0] - 2025-10-09

### Added
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<img src="./assets/banner.webp" alt="Folder Structure Pro" style="border: 4px solid rgba(255, 255, 255, 0.9); border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">

<p align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=iamshreydxv.copy-folder-structure">
<img src="https://img.shields.io/visual-studio-marketplace/v/iamshreydxv.copy-folder-structure" alt="Marketplace Version"/>
<a href="https://github.com/ShreyPurohit/folder-structure-pro-vscode/releases">
<img src="https://img.shields.io/github/last-commit/ShreyPurohit/folder-structure-pro-vscode" alt="Last Commit"/>
</a>
<a href="https://marketplace.visualstudio.com/items?itemName=iamshreydxv.copy-folder-structure">
<img src="https://img.shields.io/visual-studio-marketplace/d/iamshreydxv.copy-folder-structure" alt="Downloads"/>
<a href="https://github.com/ShreyPurohit/folder-structure-pro-vscode/issues">
<img src="https://img.shields.io/github/issues/ShreyPurohit/folder-structure-pro-vscode" alt="Open Issues"/>
</a>
<a href="https://marketplace.visualstudio.com/items?itemName=iamshreydxv.copy-folder-structure">
<img src="https://img.shields.io/visual-studio-marketplace/r/iamshreydxv.copy-folder-structure" alt="Ratings"/>
<a href="https://github.com/ShreyPurohit/folder-structure-pro-vscode/stargazers">
<img src="https://img.shields.io/github/stars/ShreyPurohit/folder-structure-pro-vscode" alt="GitHub Stars"/>
</a>
<a href="https://github.com/sponsors/ShreyPurohit">
<img src="https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-ff69b4?style=flat-square&logo=github" alt="Sponsor me on GitHub" />
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Folder Structure Pro",
"description": "Easily copy & create folder structures, file names and jump-ready line paths with a single click.",
"publisher": "iamshreydxv",
"version": "0.2.0",
"version": "0.2.1",
"engines": {
"vscode": "^1.54.0"
},
Expand Down
2 changes: 1 addition & 1 deletion src/commands/copyStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function copyStructure(uri: vscode.Uri): Promise<void> {
target = pick[0];
}

const structure = await StructureService.getStructure(target.fsPath);
const structure = await StructureService.getStructure(target);
const outputFormat = vscode.workspace
.getConfiguration('folderStructure')
.get<OutputFormat>('outputFormat', DEFAULT_OUTPUT_FORMAT);
Expand Down
13 changes: 5 additions & 8 deletions src/commands/createStructure.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { ERROR_MESSAGES } from '../constants';
import { StructureService } from '../services/structure';
Expand All @@ -7,8 +6,6 @@ import { OutputFormat, WebviewMessage } from '../types';
import { createStructureInputPanel } from '../ui/webview';
import { DEFAULT_OUTPUT_FORMAT } from '../constants';

const FORMAT_OPTIONS: OutputFormat[] = ['Plain Text Format', 'JSON Format'];

export async function createStructure(): Promise<void> {
try {
const defaultUri = vscode.workspace.workspaceFolders?.[0]?.uri;
Expand All @@ -20,8 +17,8 @@ export async function createStructure(): Promise<void> {
openLabel: 'Select target folder',
});

const resolvedPath = pick?.[0]?.fsPath;
if (!resolvedPath) {
const resolvedUri = pick?.[0];
if (!resolvedUri) {
throw new Error(ERROR_MESSAGES.TARGET_REQUIRED);
}

Expand Down Expand Up @@ -113,7 +110,7 @@ export async function createStructure(): Promise<void> {

const existing: string[] = [];
for (const name of targets) {
const full = path.join(resolvedPath, name);
const full = vscode.Uri.joinPath(resolvedUri, name);
if (await FileSystemService.exists(full)) {
existing.push(name);
}
Expand All @@ -132,7 +129,7 @@ export async function createStructure(): Promise<void> {
}
if (selection === 'Replace') {
for (const name of existing) {
const full = path.join(resolvedPath, name);
const full = vscode.Uri.joinPath(resolvedUri, name);
await FileSystemService.delete(full, {
recursive: true,
useTrash: true,
Expand All @@ -143,7 +140,7 @@ export async function createStructure(): Promise<void> {
}

await StructureService.createStructure(
resolvedPath,
resolvedUri,
message.text,
currentFormat,
);
Expand Down
9 changes: 4 additions & 5 deletions src/services/gitignore.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import ignore from 'ignore';
import * as path from 'path';
import * as vscode from 'vscode';
import { FileSystemService } from './fileSystem';

export class GitignoreService {
static async loadRules(dirPath: string): Promise<string[]> {
static async loadRules(dirUri: vscode.Uri): Promise<string[]> {
const config = vscode.workspace.getConfiguration('folderStructure');
const ignorePatterns = config.get<string[]>('ignorePatterns', ['node_modules', '.*']);
const respectGitignore = config.get<boolean>('respectGitignore', true);

let rules = [...ignorePatterns];

if (respectGitignore) {
const gitignorePath = path.join(dirPath, '.gitignore');
if (await FileSystemService.exists(gitignorePath)) {
const content = await FileSystemService.readFile(gitignorePath);
const gitignoreUri = vscode.Uri.joinPath(dirUri, '.gitignore');
if (await FileSystemService.exists(gitignoreUri)) {
const content = await FileSystemService.readFile(gitignoreUri);
rules = rules.concat(
content.split('\n').filter((line) => line.trim() && !line.startsWith('#')),
);
Expand Down
50 changes: 32 additions & 18 deletions src/services/structure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,48 @@ import { GitignestFormatter, JsonFormatter } from './formatters';
import { GitignoreService } from './gitignore';

export class StructureService {
static async getStructure(dirPath: string): Promise<FolderStructure> {
const ignoreRules = await GitignoreService.loadRules(dirPath);
private static relativeFromRoot(rootUri: vscode.Uri, targetUri: vscode.Uri): string {
const root = rootUri.path.replace(/\/+$/, '');
const target = targetUri.path;
if (!target.startsWith(root)) {
return target.replace(/^\/+/, '');
}
return target.slice(root.length).replace(/^\/+/, '');
}

private static uriBaseName(uri: vscode.Uri): string {
const segments = uri.path.split('/').filter(Boolean);
return segments[segments.length - 1] ?? uri.path;
}

static async getStructure(dirUri: vscode.Uri): Promise<FolderStructure> {
const ignoreRules = await GitignoreService.loadRules(dirUri);
const ig = ignore().add(ignoreRules);
const folderName = path.basename(dirPath);
const folderName = this.uriBaseName(dirUri);
return {
[folderName]: await this.buildStructure(dirPath, ig, dirPath),
[folderName]: await this.buildStructure(dirUri, ig, dirUri),
};
}

static async buildStructure(
dirPath: string,
dirUri: vscode.Uri,
ig: ReturnType<typeof ignore>,
rootPath: string,
rootUri: vscode.Uri,
): Promise<FolderStructure> {
const structure: FolderStructure = {};
const entries = await FileSystemService.readdir(dirPath);
const entries = await FileSystemService.readdir(dirUri);

for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
const relFromRoot = path.relative(rootPath, fullPath);
const fullUri = vscode.Uri.joinPath(dirUri, entry.name);
const relFromRoot = this.relativeFromRoot(rootUri, fullUri);

if (entry.name.startsWith('.') || ig.ignores(relFromRoot)) {
continue;
}

const isDir = (entry.type & vscode.FileType.Directory) === vscode.FileType.Directory;
if (isDir) {
structure[entry.name] = await this.buildStructure(fullPath, ig, rootPath);
structure[entry.name] = await this.buildStructure(fullUri, ig, rootUri);
} else {
const ext = this.fileTypeFor(entry.name);
const base = this.baseNameFor(entry.name);
Expand All @@ -60,7 +74,7 @@ export class StructureService {
}

static async createStructure(
basePath: string,
baseUri: vscode.Uri,
content: string,
format: OutputFormat,
): Promise<void> {
Expand All @@ -69,7 +83,7 @@ export class StructureService {
}

if (format === 'Plain Text Format') {
await this.createFromPlainText(basePath, content);
await this.createFromPlainText(baseUri, content);
} else {
try {
const structure = JSON.parse(content);
Expand All @@ -78,14 +92,14 @@ export class StructureService {
'Invalid JSON structure: use nested objects for folders and string file types for files',
);
}
await this.createFromJSON(basePath, structure);
await this.createFromJSON(baseUri, structure);
} catch (error) {
throw new Error('Invalid JSON format');
}
}
}

private static async createFromPlainText(basePath: string, content: string): Promise<void> {
private static async createFromPlainText(baseUri: vscode.Uri, content: string): Promise<void> {
const rawLines = content.split('\n');
// Ignore the first line (header/title) regardless of its text
const lines = rawLines
Expand All @@ -107,7 +121,7 @@ export class StructureService {
pathStack.pop();
}

const fullPath = path.join(basePath, ...pathStack, node.name);
const fullPath = vscode.Uri.joinPath(baseUri, ...pathStack, node.name);

if (node.isDirectory) {
await FileSystemService.mkdirIfAbsent(fullPath);
Expand All @@ -126,16 +140,16 @@ export class StructureService {
}

private static async createFromJSON(
basePath: string,
baseUri: vscode.Uri,
structure: FolderStructure,
): Promise<void> {
for (const [key, value] of Object.entries(structure)) {
if (typeof value === 'string') {
const fileName = value === 'file' || value.trim() === '' ? key : `${key}.${value}`;
const fullPath = path.join(basePath, fileName);
const fullPath = vscode.Uri.joinPath(baseUri, fileName);
await FileSystemService.writeFileIfAbsent(fullPath, '');
} else {
const dirPath = path.join(basePath, key);
const dirPath = vscode.Uri.joinPath(baseUri, key);
await FileSystemService.mkdirIfAbsent(dirPath);
await this.createFromJSON(dirPath, value);
}
Expand Down
Loading