Skip to content

Commit

Permalink
feat: allow dragging sas content into editor (#510)
Browse files Browse the repository at this point in the history
**Summary**
This adds the ability to drag sas content into sas programs. See #329 for more details

**Testing**
 - [x] Tested dragging a folder into a SAS program (should not provide any code)
 - [x] Tested dragging a file into a SAS program from my folder
 - [x] Tested dragging a file into a SAS program from my favorites
 - [x] Tested dragging files into a SAS program from nested folder paths
 - [x] Tested dragging two files from the same folder into a SAS program
  • Loading branch information
scottdover authored Oct 17, 2023
1 parent bf30847 commit 4c172f6
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 4 deletions.
40 changes: 39 additions & 1 deletion client/src/components/ContentNavigator/ContentDataProvider.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
// Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import {
CancellationToken,
DataTransfer,
DataTransferItem,
Disposable,
DocumentDropEdit,
Event,
EventEmitter,
FileChangeEvent,
FileStat,
FileSystemProvider,
FileType,
Position,
ProviderResult,
Tab,
TabInputNotebook,
TabInputText,
TextDocument,
TextDocumentContentProvider,
ThemeIcon,
TreeDataProvider,
Expand All @@ -24,6 +28,7 @@ import {
Uri,
commands,
l10n,
languages,
window,
} from "vscode";

Expand All @@ -45,6 +50,7 @@ import { convertNotebookToFlow } from "./convert";
import { ContentItem } from "./types";
import {
getCreationDate,
getFileStatement,
getId,
isContainer as getIsContainer,
getLabel,
Expand All @@ -71,6 +77,7 @@ class ContentDataProvider
private _onDidChangeTreeData: EventEmitter<ContentItem | undefined>;
private _onDidChange: EventEmitter<Uri>;
private _treeView: TreeView<ContentItem>;
private _dropEditProvider: Disposable;
private readonly model: ContentModel;
private extensionUri: Uri;

Expand All @@ -93,6 +100,10 @@ class ContentDataProvider
dragAndDropController: this,
canSelectMany: true,
});
this._dropEditProvider = languages.registerDocumentDropEditProvider(
{ language: "sas" },
this,
);

this._treeView.onDidChangeVisibility(async () => {
if (this._treeView.visible) {
Expand Down Expand Up @@ -140,8 +151,35 @@ class ContentDataProvider
dataTransfer.set(this.dragMimeTypes[0], dataTransferItem);
}

public async provideDocumentDropEdits(
document: TextDocument,
position: Position,
dataTransfer: DataTransfer,
token: CancellationToken,
): Promise<DocumentDropEdit | undefined> {
const dataTransferItem = dataTransfer.get(this.dragMimeTypes[0]);
const contentItem =
dataTransferItem && JSON.parse(dataTransferItem.value)[0];
if (token.isCancellationRequested || !contentItem) {
return undefined;
}

const fileFolderPath = await this.model.getFileFolderPath(contentItem);
if (!fileFolderPath) {
return undefined;
}

return {
insertText: getFileStatement(
contentItem.name,
document.getText(),
fileFolderPath,
),
};
}

public getSubscriptions(): Disposable[] {
return [this._treeView];
return [this._treeView, this._dropEditProvider];
}

get onDidChangeFile(): Event<FileChangeEvent[]> {
Expand Down
24 changes: 24 additions & 0 deletions client/src/components/ContentNavigator/ContentModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,30 @@ export class ContentModel {
}
return "unknown";
}

public async getFileFolderPath(contentItem: ContentItem): Promise<string> {
if (isContainer(contentItem)) {
return "";
}

const filePathParts = [];
let currentContentItem: Pick<ContentItem, "parentFolderUri" | "name"> =
contentItem;
do {
try {
const { data: parentData } = await this.connection.get(
currentContentItem.parentFolderUri,
);
currentContentItem = parentData;
} catch (e) {
return "";
}

filePathParts.push(currentContentItem.name);
} while (currentContentItem.parentFolderUri);

return "/" + filePathParts.reverse().join("/");
}
}

const getPermission = (item: ContentItem): Permission => {
Expand Down
26 changes: 25 additions & 1 deletion client/src/components/ContentNavigator/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { Uri } from "vscode";
import { SnippetString, Uri } from "vscode";

import {
FILE_TYPE,
Expand Down Expand Up @@ -111,3 +111,27 @@ export const isItemInRecycleBin = (item: ContentItem): boolean =>
!!item && item.flags?.isInRecycleBin;

export const isContentItem = (item): item is ContentItem => isValidItem(item);

// A document uses uppercase letters _if_ are no words
// (where word means gte 3 characters) that are lowercase.
const documentUsesUppercase = (documentContent: string) =>
documentContent &&
!documentContent
// Exclude anything in quotes from our calculations
.replace(/('|")([^('|")]*)('|")/g, "")
.match(/([a-z]{3,})\S/g);

export const getFileStatement = (
contentItemName: string,
documentContent: string,
fileFolderPath: string,
): SnippetString => {
const usesUppercase = documentUsesUppercase(documentContent);
const cmd = "filename ${1:fileref} filesrvc folderpath='$1' filename='$2';\n";

return new SnippetString(
(usesUppercase ? cmd.toUpperCase() : cmd)
.replace("$1", fileFolderPath.replace(/'/g, "''"))
.replace("$2", contentItemName.replace(/'/g, "''")),
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -722,8 +722,6 @@ describe("ContentDataProvider", async function () {
const dataTransferItem = new DataTransferItem(uri);
dataTransfer.set("text/uri-list", dataTransferItem);

console.log("this bithc");

stub.returns(new Promise((resolve) => resolve(item)));

await dataProvider.handleDrop(parentItem, dataTransfer);
Expand Down Expand Up @@ -889,4 +887,67 @@ describe("ContentDataProvider", async function () {
expect(stub.calledWith(item, getLink(parentItem.links, "GET", "self")?.uri))
.to.be.true;
});

it("getFileFolderPath - returns empty path for folder", async function () {
const item = mockContentItem({
type: "folder",
name: "folder",
});

const model = new ContentModel();
const dataProvider = new ContentDataProvider(
model,
Uri.from({ scheme: "http" }),
);

await dataProvider.connect("http://test.io");
const path = await model.getFileFolderPath(item);

expect(path).to.equal("");
});

it("getFileFolderPath - traverses parentFolderUri to find path", async function () {
const grandparent = mockContentItem({
type: "folder",
name: "grandparent",
id: "/id/grandparent",
});
const parent = mockContentItem({
type: "folder",
name: "parent",
id: "/id/parent",
parentFolderUri: "/id/grandparent",
});
const item = mockContentItem({
type: "file",
name: "file.sas",
parentFolderUri: "/id/parent",
});
const item2 = mockContentItem({
type: "file",
name: "file2.sas",
parentFolderUri: "/id/parent",
});

const model = new ContentModel();
const dataProvider = new ContentDataProvider(
model,
Uri.from({ scheme: "http" }),
);

axiosInstance.get.withArgs("/id/parent").resolves({
data: parent,
});
axiosInstance.get.withArgs("/id/grandparent").resolves({
data: grandparent,
});

await dataProvider.connect("http://test.io");

// We expect both files to have the same folder path
expect(await model.getFileFolderPath(item)).to.equal("/grandparent/parent");
expect(await model.getFileFolderPath(item2)).to.equal(
"/grandparent/parent",
);
});
});
27 changes: 27 additions & 0 deletions client/test/components/ContentNavigator/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect } from "chai";

import { getFileStatement } from "../../../src/components/ContentNavigator/utils";

describe("utils", async function () {
it("getFileStatement - returns extensionless name + numeric suffix with no content", () => {
expect(getFileStatement("testcsv.csv", "", "/path").value).to.equal(
`filename \${1:fileref} filesrvc folderpath='/path' filename='testcsv.csv';\n`,
);
});

it("getFileStatement - returns uppercase name + suffix with uppercase content", () => {
expect(
getFileStatement("testcsv.csv", "UPPER CASE CONTENT", "/path").value,
).to.equal(
`FILENAME \${1:FILEREF} FILESRVC FOLDERPATH='/path' FILENAME='testcsv.csv';\n`,
);
});

it("getFileStatement - returns encoded filename when filename contains quotes", () => {
expect(
getFileStatement("testcsv-'withquotes'.csv", "", "/path").value,
).to.equal(
`filename \${1:fileref} filesrvc folderpath='/path' filename='testcsv-''withquotes''.csv';\n`,
);
});
});

0 comments on commit 4c172f6

Please sign in to comment.