Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Add R language support for PROC R (syntax highlighting, notebook cells, code formatting preservation) ([#1719](https://github.com/sassoftware/vscode-sas-extension/pull/1719))
- Add the ability to pin columns ([#1781](https://github.com/sassoftware/vscode-sas-extension/pull/1781))
- Add support for deleting multiple files and folders at once ([#1846](https://github.com/sassoftware/vscode-sas-extension/pull/1846))

## [1.19.1] - 2026-04-01

Expand Down
53 changes: 33 additions & 20 deletions client/src/components/ContentNavigator/ContentDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,18 @@ class ContentDataProvider
return this.model.saveContentToUri(uri, new TextDecoder().decode(content));
}

public async deleteResource(item: ContentItem): Promise<boolean> {
if (!(await closeFileIfOpen(item))) {
return false;
public async closeResourceFiles(items: ContentItem[]): Promise<boolean> {
for (const item of items) {
const result = await closeFileIfOpen(item);
if (result === false) {
// User canceled the save dialog
return false;
}
}
return true;
}

public async deleteResource(item: ContentItem): Promise<boolean> {
const success = await this.model.delete(item);
if (success) {
this.refresh();
Expand All @@ -379,10 +387,6 @@ class ContentDataProvider
}

public async recycleResource(item: ContentItem): Promise<boolean> {
if (!(await closeFileIfOpen(item))) {
return false;
}

const { newUri, oldUri } = await this.model.recycleResource(item);

if (newUri) {
Expand Down Expand Up @@ -847,23 +851,32 @@ class ContentDataProvider

export default ContentDataProvider;

const closeFileIfOpen = (item: ContentItem): Promise<Uri[]> | boolean => {
const closeFileIfOpen = (item: ContentItem): Promise<Uri[] | false> | true => {
const tabs = getEditorTabsForItem(item);
if (tabs.length > 0) {
return new Promise((resolve, reject) => {
Promise.all(tabs.map((tab) => window.tabGroups.close(tab)))
.then(() =>
resolve(
tabs
.map(
(tab) =>
(tab.input instanceof TabInputText ||
tab.input instanceof TabInputNotebook) &&
tab.input.uri,
)
.filter((exists) => exists),
),
)
.then((results) => {
// Check if all tabs were successfully closed
const allClosed = results.every((result) => result === true);
if (!allClosed) {
// User canceled the save dialog
resolve(false);
return;
}
// All tabs closed successfully, return their URIs
const closedUris: Uri[] = [];
for (const tab of tabs) {
if (
(tab.input instanceof TabInputText ||
tab.input instanceof TabInputNotebook) &&
tab.input.uri
) {
closedUris.push(tab.input.uri);
}
}
resolve(closedUris);
})
.catch(reject);
});
}
Expand Down
12 changes: 12 additions & 0 deletions client/src/components/ContentNavigator/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,21 @@ export const Messages = {
DeleteWarningMessage: l10n.t(
'Are you sure you want to permanently delete the item "{name}"?',
),
DeleteMultipleWarningMessage: l10n.t(
"Are you sure you want to permanently delete these {count} items?\n\n{items}",
),
RecycleWarningMessage: l10n.t(
'Are you sure you want to move the item "{name}" to the Recycle Bin?',
),
RecycleDirtyFolderWarning: l10n.t(
"This folder contains unsaved files, are you sure you want to delete?",
),
RecycleMultipleWarningMessage: l10n.t(
"Are you sure you want to move these {count} items to the Recycle Bin?\n\n{items}",
),
RecycleMultipleDirtyWarning: l10n.t(
"Some folders contain unsaved files. Are you sure you want to move these {count} items to the Recycle Bin?\n\n{items}",
),
EmptyRecycleBinError: l10n.t("Unable to empty the recycle bin."),
EmptyRecycleBinWarningMessage: l10n.t(
"Are you sure you want to permanently delete all the items? You cannot undo this action.",
Expand Down
178 changes: 132 additions & 46 deletions client/src/components/ContentNavigator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,54 +102,140 @@ class ContentNavigator implements SubscriptionProvider {
commands.registerCommand(
`${SAS}.deleteResource`,
async (item: ContentItem) => {
this.getTreeViewSelections(item).forEach(
async (resource: ContentItem) => {
if (!resource.contextValue.includes("delete")) {
return;
}
const isContainer = getIsContainer(resource);
const hasUnsavedFiles = isContainer
? await this.contentDataProvider.checkFolderDirty(resource)
: false;
const moveToRecycleBin =
this.contentDataProvider.canRecycleResource(resource);
const selections = this.getTreeViewSelections(item);

if (
!moveToRecycleBin &&
!(await window.showWarningMessage(
l10n.t(Messages.DeleteWarningMessage, {
name: resource.name,
}),
{ modal: true },
Messages.DeleteButtonLabel,
))
) {
return;
} else if (moveToRecycleBin && hasUnsavedFiles) {
if (
!(await window.showWarningMessage(
l10n.t(Messages.RecycleDirtyFolderWarning, {
name: resource.name,
}),
{ modal: true },
Messages.MoveToRecycleBinLabel,
))
) {
return;
}
}
const deleteResult = moveToRecycleBin
? await this.contentDataProvider.recycleResource(resource)
: await this.contentDataProvider.deleteResource(resource);
if (!deleteResult) {
window.showErrorMessage(
isContainer
? Messages.FolderDeletionError
: Messages.FileDeletionError,
);
}
},
const deletableItems = selections.filter((resource: ContentItem) =>
resource.contextValue?.includes("delete"),
);

if (deletableItems.length === 0) {
return;
}

// Close all open files first and handle unsaved changes
// If user cancels the save dialog, abort the deletion
if (
!(await this.contentDataProvider.closeResourceFiles(deletableItems))
) {
return;
}

const recyclableItems = deletableItems.filter(
(resource: ContentItem) =>
this.contentDataProvider.canRecycleResource(resource),
);
const permanentDelete =
deletableItems.length > recyclableItems.length;

let hasUnsavedFiles = false;
for (const resource of deletableItems) {
const isContainer = getIsContainer(resource);
if (
isContainer &&
(await this.contentDataProvider.checkFolderDirty(resource))
) {
hasUnsavedFiles = true;
break;
}
}

let confirmed = false;
if (deletableItems.length === 1) {
const resource = deletableItems[0];
const isContainer = getIsContainer(resource);
const canRecycle =
this.contentDataProvider.canRecycleResource(resource);
const itemHasUnsavedFiles = isContainer
? await this.contentDataProvider.checkFolderDirty(resource)
: false;

if (!canRecycle) {
confirmed = !!(await window.showWarningMessage(
l10n.t(Messages.DeleteWarningMessage, {
name: resource.name,
}),
{ modal: true },
Messages.DeleteButtonLabel,
));
} else if (itemHasUnsavedFiles) {
confirmed = !!(await window.showWarningMessage(
l10n.t(Messages.RecycleDirtyFolderWarning, {
name: resource.name,
}),
{ modal: true },
Messages.MoveToRecycleBinLabel,
));
} else {
confirmed = !!(await window.showWarningMessage(
l10n.t(Messages.RecycleWarningMessage, {
name: resource.name,
}),
{ modal: true },
Messages.MoveToRecycleBinLabel,
));
}
} else {
const maxDisplayItems = 10;
const itemNames = deletableItems
.slice(0, maxDisplayItems)
.map((item) => ` • ${item.name}`)
.join("\n");
const remainingItemsCount = deletableItems.length - maxDisplayItems;
const itemsList =
remainingItemsCount > 0
? `${itemNames}\n • ...and ${remainingItemsCount} more`
: itemNames;

if (permanentDelete) {
confirmed = !!(await window.showWarningMessage(
l10n.t(Messages.DeleteMultipleWarningMessage, {
count: deletableItems.length,
items: itemsList,
}),
{ modal: true },
Messages.DeleteButtonLabel,
));
} else if (hasUnsavedFiles) {
confirmed = !!(await window.showWarningMessage(
l10n.t(Messages.RecycleMultipleDirtyWarning, {
count: deletableItems.length,
items: itemsList,
}),
{ modal: true },
Messages.MoveToRecycleBinLabel,
));
} else {
confirmed = !!(await window.showWarningMessage(
l10n.t(Messages.RecycleMultipleWarningMessage, {
count: deletableItems.length,
items: itemsList,
}),
{ modal: true },
Messages.MoveToRecycleBinLabel,
));
}
}

if (!confirmed) {
return;
}

for (const resource of deletableItems) {
const isContainer = getIsContainer(resource);
const canRecycle =
this.contentDataProvider.canRecycleResource(resource);
const deleteResult = canRecycle
? await this.contentDataProvider.recycleResource(resource)
: await this.contentDataProvider.deleteResource(resource);

if (!deleteResult) {
window.showErrorMessage(
isContainer
? Messages.FolderDeletionError
: Messages.FileDeletionError,
);
}
}
},
),
commands.registerCommand(
Expand Down
Loading