Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9622e8b
feat: adding multi delete/recycle messages
Wizzzzzzard Apr 2, 2026
ff9640f
refactor: refactor deleteResource to show a single delete confirmatio…
Wizzzzzzard Apr 2, 2026
c5faa41
refactor: running prettier format
Wizzzzzzard Apr 2, 2026
6fab3de
fix: removing unused moveToRecycleBin var
Wizzzzzzard Apr 2, 2026
8652e7d
feat: updating messages to include list of items to be deleted/recycled
Wizzzzzzard Apr 2, 2026
d25c042
feat: updating delete to output a list of files/items to be deleted
Wizzzzzzard Apr 2, 2026
f5fd7d8
refactor: running prettier formatting
Wizzzzzzard Apr 2, 2026
f9188c6
Merge branch 'main' into feat/improve-multiple-deletion-process
Wizzzzzzard Apr 6, 2026
30ac8f0
Merge branch 'main' of github.com:sassoftware/vscode-sas-extension in…
Wizzzzzzard Apr 6, 2026
50787d3
Merge branch 'feat/improve-multiple-deletion-process' of personal:Wiz…
Wizzzzzzard Apr 6, 2026
66e10e8
fix: correcting bug where SAS Server would ask to delete a file befor…
Wizzzzzzard Apr 14, 2026
ece21e7
fix: prompting to recycle on unsaved SAS Content files rather than im…
Wizzzzzzard Apr 14, 2026
d844cc7
chore: updating changelog with multi delete feature
Wizzzzzzard Apr 14, 2026
e42effc
Merge branch 'main' of github.com:sassoftware/vscode-sas-extension in…
Wizzzzzzard May 1, 2026
d2ca4b2
refactor: do not confirm when moving items to recycle bin
Wizzzzzzard May 1, 2026
1e3aa50
refactor: simplify multiple deletion confirmation to count only
Wizzzzzzard May 1, 2026
66a5d61
refactor: unify dirty files warning into a single message
Wizzzzzzard May 1, 2026
15be2f1
refactor: making vague comment more explicit
Wizzzzzzard May 1, 2026
a409fa3
Merge branch 'main' of github.com:sassoftware/vscode-sas-extension in…
Wizzzzzzard May 1, 2026
5698837
refactor: making single Delete Warning message read slightly better
Wizzzzzzard May 1, 2026
344e9dd
Merge branch 'main' into feat/improve-multiple-deletion-process
Wizzzzzzard May 6, 2026
fef68db
Merge branch 'main' into feat/improve-multiple-deletion-process
Wizzzzzzard May 12, 2026
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 @@ -11,6 +11,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))
- Allow running R code files in SAS Content ([#1845](https://github.com/sassoftware/vscode-sas-extension/pull/1845))
- 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) {
// At least one tab could not be closed, e.g. the user declined to save
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
9 changes: 6 additions & 3 deletions client/src/components/ContentNavigator/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,13 @@ export const Messages = {
DeleteButtonLabel: l10n.t("Delete"),
MoveToRecycleBinLabel: l10n.t("Move to Recycle Bin"),
DeleteWarningMessage: l10n.t(
'Are you sure you want to permanently delete the item "{name}"?',
'Are you sure you want to permanently delete "{name}"?',
),
RecycleDirtyFolderWarning: l10n.t(
"This folder contains unsaved files, are you sure you want to delete?",
DeleteMultipleWarningMessage: l10n.t(
"Are you sure you want to permanently delete {count} items?",
),
DirtyFolderWarning: l10n.t(
"The selected folders contain unsaved files, are you sure you want to delete?",
),
EmptyRecycleBinError: l10n.t("Unable to empty the recycle bin."),
EmptyRecycleBinWarningMessage: l10n.t(
Expand Down
152 changes: 106 additions & 46 deletions client/src/components/ContentNavigator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,54 +102,114 @@ 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 =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to understand this a bit more. From my general understanding, I would suspect permanentDelete means we have something that cannot be recycled. Could this be simplified to something like...

const permanentDelete = deletableItems.some(
            (resource: ContentItem) =>
              !this.contentDataProvider.canRecycleResource(resource),
);

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;
Comment on lines +148 to +150

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this different than hasUnsavedFiles? Presumably, if we have a single deletable item, then it should've have already triggered this same code up on line 133-137. I think we can probably clean this up.


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.DirtyFolderWarning),
{ modal: true },
canRecycle
? Messages.MoveToRecycleBinLabel
: Messages.DeleteButtonLabel,
));
} else {
confirmed = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we default to confirmed = true here and down on line 189, I'm wondering if it makes more sense to get rid of this else case and the one below and instead start w/ let confirmed = true;

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

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