Skip to content

Commit

Permalink
Merge branch 'main' into feat/reorg-utils
Browse files Browse the repository at this point in the history
  • Loading branch information
scottdover authored Oct 18, 2023
2 parents 2fa0075 + 4c172f6 commit f7f94dc
Show file tree
Hide file tree
Showing 12 changed files with 658 additions and 110 deletions.
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,11 @@ Follow these steps to update a locale for the SAS Extension for VSCode:
- Update any untranslated strings.
- Verify your changes using `Launch Client`.
- After you've verified changes, you can create a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) for review.

### Locale contributors

| Language | VSCode Language ID | Contributor |
| ------------------------ | ------------------ | ----------- |
| **German** | de | David Weik |
| **Chinese (Simplified)** | zh-cn | Wei Wu |
| **Portuguese (Brazil)** | pt-br | Mark Jordan |
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`,
);
});
});
115 changes: 115 additions & 0 deletions l10n/bundle.l10n.ko.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"Are you sure you want to permanently delete all the items? You cannot undo this action.": "모든 항목을 영구적으로 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
"Are you sure you want to permanently delete the item \"{name}\"?": "항목 \"{name}\"을(를) 영구적으로 삭제하시겠습니까?",
"Cancelling job...": "작업 취소 중...",
"Cannot call self on ComputeSession with no id": "id가 없는 ComputeSession에서 self를 호출할 수 없습니다",
"Cannot call self on object with no id": "id가 없는 개체에서 self를 호출할 수 없습니다",
"Cannot connect to SAS Studio service": "SAS Studio 서비스에 연결할 수 없습니다",
"Cannot find file: {file}": "파일을 찾을 수 없습니다: {file}",
"Client ID": "클라이언트 ID",
"Client Secret": "클라이언트 비밀번호",
"Compute Context not found: {name}": "컴퓨팅 컨텍스트를 찾을 수 없음: {name}",
"Connecting to SAS session...": "SAS 세션에 연결 중...",
"Connection Type": "연결 유형",
"Delete": "삭제",
"Enter a client ID": "클라이언트 ID를 입력하세요",
"Enter a client secret": "클라이언트 비밀번호를 입력하세요",
"Enter a file name.": "파일 이름을 입력하세요.",
"Enter a folder name.": "폴더 이름을 입력하세요.",
"Enter a name for the new .flw file": "새 .flw 파일의 이름을 입력하세요",
"Enter a new name.": "새 이름을 입력하세요.",
"Enter a port number": "포트 번호를 입력하세요",
"Enter a port number.": "포트 번호를 입력하세요.",
"Enter connection name": "연결 이름을 입력하세요",
"Enter secret for client ID. An example is myapp.secret.": "클라이언트 ID의 비밀번호를 입력하세요. 예시: myapp.secret.",
"Enter the SAS compute context": "SAS 컴퓨팅 컨텍스트를 입력하세요",
"Enter the SAS compute context.": "SAS 컴퓨팅 컨텍스트를 입력하세요.",
"Enter the URL": "URL을 입력하세요",
"Enter the URL for the SAS Viya server. An example is https://example.sas.com.": "SAS Viya 서버의 URL을 입력하세요. 예시: https://example.sas.com",
"Enter the name of the SAS 9 SSH server.": "SAS 9 SSH 서버의 이름을 입력하세요.",
"Enter the registered client ID. An example is myapp.client.": "등록된 클라이언트 ID를 입력하세요. 예시: myapp.client.",
"Enter the server name": "서버 이름을 입력하세요",
"Enter the server path": "서버 경로를 입력하세요",
"Enter the server path of the SAS Executable.": "SAS 실행 파일의 서버 경로를 입력하세요.",
"Enter your SAS server username.": "SAS 서버 사용자 이름을 입력하세요.",
"Enter your username": "사용자 이름을 입력하세요",
"Error converting the notebook file to .flw format.": "노트북 파일을 .flw 형식으로 변환하는 중 오류가 발생했습니다.",
"Error getting server with ID {id} - {message}": "ID {id} - {message} 서버를 가져오는 중 오류 발생",
"Error getting session with ID {id} - {message}": "ID {id} - {message} 세션을 가져오는 중 오류 발생",
"Failed to connect to Session. Check profile settings.": "세션에 연결하지 못했습니다. 프로필 설정을 확인하세요.",
"Failed to get state from Session {sessionId}": "{sessionId} 세션에서 상태를 가져오지 못했습니다",
"File added to my folder.": "파일이 내 폴더에 추가되었습니다.",
"Invalid connectionType. Check Profile settings.": "유효하지 않은 연결 유형입니다. 프로필 설정을 확인하세요.",
"Invalid file name.": "유효하지 않은 파일 이름입니다.",
"Job does not have '{linkName}' link": "작업에 '{linkName}' 링크가 없습니다",
"Method not implemented.": "메서드가 구현되지 않았습니다.",
"Missing connectionType in active profile.": "활성 프로필에 연결 유형이 누락되었습니다.",
"Missing endpoint in active profile.": "활성 프로필에 엔드포인트가 누락되었습니다.",
"Missing host in active profile.": "활성 프로필에 호스트가 누락되었습니다.",
"Missing port in active profile.": "활성 프로필에 포트가 누락되었습니다.",
"Missing sas path in active profile.": "활성 프로필에 SAS 경로가 누락되었습니다.",
"Missing username in active profile.": "활성 프로필에 사용자 이름이 누락되었습니다.",
"New File": "새 파일",
"New Folder": "새 폴더",
"New SAS Connection Profile Name": "새 SAS 연결 프로필 이름",
"No Active Profile": "활성 프로필 없음",
"No Profile": "프로필 없음",
"No Profiles available to delete": "삭제할 수 있는 프로필이 없습니다",
"No SAS Connection Profile": "SAS 연결 프로필 없음",
"No authorization code": "인증 코드 없음",
"No opened file": "열린 파일 없음",
"No valid sas code": "유효한 sas 코드 없음",
"Not a valid sas file: {file}": "유효한 SAS 파일이 아닙니다: {file}",
"Not implemented": "구현되지 않음",
"Paste authorization code here": "여기에 인증 코드 붙여넣기",
"Port Number": "포트 번호",
"Rename File": "파일 이름 변경",
"Rename Folder": "폴더 이름 변경",
"Result": "결과",
"Result: {result}": "결과: {result}",
"SAS 9 SSH Server": "SAS 9 SSH 서버",
"SAS Compute Context": "SAS 컴퓨팅 컨텍스트",
"SAS Log": "SAS 로그",
"SAS Server Username": "SAS 서버 사용자 이름",
"SAS Viya Server": "SAS Viya 서버",
"SAS code running...": "SAS 코드 실행 중...",
"SSH_AUTH_SOCK not set. Check Environment Variables.": "SSH_AUTH_SOCK가 설정되지 않았습니다. 환경 변수를 확인하세요.",
"Saving {itemName}.": "{itemName} 저장 중...",
"Select a Connection Type": "연결 유형 선택",
"Select a Connection Type.": "연결 유형을 선택하십시오.",
"Select a SAS connection profile": "SAS 연결 프로필 선택",
"Server Path": "서버 경로",
"Server does not have createSession link": "서버에 createSession 링크가 없습니다",
"Server does not have state link": "서버에 상태 링크가 없습니다",
"Session does not have '{linkName}' link": "세션에 '{linkName}' 링크가 없습니다",
"Show results...": "결과 표시 중...",
"Something went wrong": "문제가 발생했습니다",
"Switch Current SAS Profile": "현재 SAS 프로필 전환",
"Task is cancelled.": "작업이 취소되었습니다.",
"Task is complete.": "작업이 완료되었습니다.",
"The SAS session has closed.": "SAS 세션이 종료되었습니다.",
"The file type is unsupported.": "지원되지 않는 파일 형식입니다.",
"The folder name cannot contain more than 100 characters.": "폴더 이름은 100자를 초과할 수 없습니다.",
"The item could not be added to My Favorites.": "항목을 즐겨찾기에 추가할 수 없습니다.",
"The item could not be removed from My Favorites.": "항목을 즐겨찾기에서 제거할 수 없습니다.",
"The notebook file does not contain any code to convert.": "노트북 파일에 변환할 코드가 포함되어 있지 않습니다.",
"The notebook has been successfully converted to a flow. You can now open it in SAS Studio.": "노트북이 성공적으로 플로우로 변환되었습니다. 이제 SAS Studio에서 열 수 있습니다.",
"The output file name must end with the .flw extension.": "출력 파일 이름은 .flw 확장자로 끝나야 합니다.",
"The {selected} SAS connection profile has been deleted from the settings.json file.": "{selected} SAS 연결 프로필이 settings.json 파일에서 삭제되었습니다.",
"Unable to add file to my folder.": "파일을 내 폴더에 추가할 수 없습니다.",
"Unable to create file \"{name}\".": "\"{name}\" 파일을 생성할 수 없습니다.",
"Unable to create folder \"{name}\".": "\"{name}\" 폴더를 생성할 수 없습니다.",
"Unable to delete file.": "파일을 삭제할 수 없습니다.",
"Unable to delete folder.": "폴더를 삭제할 수 없습니다.",
"Unable to delete table {tableName}.": "{tableName} 테이블을 삭제할 수 없습니다.",
"Unable to drag files from my favorites.": "즐겨찾기에서 파일을 드래그할 수 없습니다.",
"Unable to drag files from trash.": "휴지통에서 파일을 드래그할 수 없습니다.",
"Unable to drop item \"{name}\".": "\"{name}\" 항목을 놓을 수 없습니다.",
"Unable to empty the recycle bin.": "휴지통을 비울 수 없습니다",
"Unable to rename \"{oldName}\" to \"{newName}\".": "\"{oldName}\"을(를) \"{newName}\" 이름으로 바꿀 수 없습니다.",
"Unable to restore file.": "파일을 복원할 수 없습니다.",
"Unable to restore folder.": "폴더를 복원할 수 없습니다.",
"View SAS Table": "SAS 테이블 보기",
"You can also specify connection profile using the settings.json file.": "settings.json 파일을 사용하여 연결 프로필을 지정할 수도 있습니다.",
"You must save your file before you can rename it.": "파일 이름을 바꾸기 전에 파일을 저장해야 합니다."
}
Loading

0 comments on commit f7f94dc

Please sign in to comment.