Skip to content
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

feat: allow dragging sas content into editor #510

Merged
merged 13 commits into from
Oct 17, 2023
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) =>
scottdover marked this conversation as resolved.
Show resolved Hide resolved
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`,
);
});
});