From 3f9059e9429b6d11b014244ca113b2a90d340d6f Mon Sep 17 00:00:00 2001 From: "Jeroen Meijer (Jay)" Date: Mon, 5 Oct 2020 22:48:11 +0200 Subject: [PATCH] feat: v2.1.0 - auto sorting & crash fixes (#58) - auto sort dependencies after import (implements #19) - add option for auto sorting - add command for auto sorting - fix crash when pubspec or dependencies are empty (fixes #56, #54, #49, #47) --- .vscode/launch.json | 54 ++++---- CHANGELOG.md | 12 ++ README.md | 11 +- package-lock.json | 51 +++---- package.json | 26 ++-- src/extension.ts | 8 +- src/functions/addDependency.ts | 195 +++++++-------------------- src/functions/index.ts | 3 +- src/functions/sortAllDependencies.ts | 57 ++++++++ src/{ => helper}/escapeHtml.ts | 0 src/helper/formatIfOpened.ts | 8 ++ src/helper/getFileContext.ts | 28 ++++ src/helper/getPubspecText.ts | 9 ++ src/helper/getSettings.ts | 2 + src/helper/messaging.ts | 20 +-- src/helper/sortDependencies.ts | 61 +++++++++ src/model/dependencyType.ts | 4 + src/model/pubApi.ts | 2 +- src/model/pubspecContext.ts | 4 +- src/test/extension.test.ts | 9 +- src/test/pubspecMockData.ts | 2 +- 21 files changed, 332 insertions(+), 234 deletions(-) create mode 100644 src/functions/sortAllDependencies.ts rename src/{ => helper}/escapeHtml.ts (100%) create mode 100644 src/helper/formatIfOpened.ts create mode 100644 src/helper/getFileContext.ts create mode 100644 src/helper/getPubspecText.ts create mode 100644 src/helper/sortDependencies.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index c174db3..dd14145 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,34 +3,28 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [ - { - "name": "Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "npm: watch" - }, - { - "name": "Extension Tests", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test" - ], - "outFiles": [ - "${workspaceFolder}/out/test/**/*.js" - ], - "preLaunchTask": "npm: watch" - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Debug extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "npm: watch" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "preLaunchTask": "npm: watch" + } + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index dd55017..74aca19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,3 +97,15 @@ First official release! 🎉 - Filtered `dart:...` packages from search results. - Improved and updated README.md - Upgrade dependency versions for security reasons (again again). + +## 2.1.0 - 2020-10-05 + +### Changed + +- Added command that sorts all `dependencies` and `dev_dependencies`. +- Added option to sorts all `dependencies` and `dev_dependencies` automatically when running an import (turned on by default). (Thanks [@JCKodel](https://github.com/JCKodel) for feature request [#19](https://github.com/jeroen-meijer/pubspec-assist/issues/19)!) +- Removed old text parser. + +### Fixed + +- Fixed several bugs where the extension would crash when adding dependencies to an empty pubspec or empty `(dev_)dependencies`. (Thank you to [@cverdes](https://github.com/cverdes) for [#56](https://github.com/jeroen-meijer/pubspec-assist/issues/56), [@JCKodel](https://github.com/JCKodel) for [#54](https://github.com/jeroen-meijer/pubspec-assist/issues/54), [@simphotonics](https://github.com/simphotonics) for [#49](https://github.com/jeroen-meijer/pubspec-assist/issues/49) and [@ernestsheldon](https://github.com/ernestsheldon) for [#47](https://github.com/jeroen-meijer/pubspec-assist/issues/47)!) diff --git a/README.md b/README.md index 98b83c4..1a74de0 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,13 @@ You can look for and import packages directly from VS Code without ever switchin **Pubspec Assist is smart about finding what you're trying to search for.**
It gets you the most likely package you want and will give you a selection to choose from otherwise and **sorts them by relevance**. +### Automatically sort dependencies. + +**Pubspec Assist automatically sorts your dependencies after adding a new package.**
+This makes it easy to manage packages from multiple sources, such as local packages, ones hosted on Git, somewhere else or regular packages from [pub.dev](https://pub.dev/). + +Already have your dependencies set up? **Just use the sort command to instantly sort your existing dependencies.** + ### Compatible with multiple projects. **Pubspec Assist will add your new dependencies to the `pubspec.yaml` that's open in your editor.**
@@ -51,8 +58,10 @@ Don't have your `pubspec.yaml` file opened? No problem - the package will be add Some features that are planned for the future, in order of expected implementation. +- Search/batch add/update multiple packages. +- Command for updating all packages. - Auto-complete while searching. ## Bugs and feature requests -If you have any bugs or feature requests to report, please check out the issues on the GitHub repository page or create a new one. +If you have any bugs or feature requests to report, please check out the issues on the GitHub repository or create a new one. diff --git a/package-lock.json b/package-lock.json index 94b8fa1..69d2b75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "pubspec-assist", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -30,27 +30,20 @@ "js-tokens": "^4.0.0" } }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, "requires": { - "@types/events": "*", "@types/minimatch": "*", "@types/node": "*" } }, "@types/js-yaml": { - "version": "3.12.3", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.3.tgz", - "integrity": "sha512-otRe77JNNWzoVGLKw8TCspKswRoQToys4tuL6XYVBFxjgeM0RUrx7m3jkaTdxILxeGry3zM8mGYkGXMeQ02guA==", + "version": "3.12.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.5.tgz", + "integrity": "sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww==", "dev": true }, "@types/minimatch": { @@ -66,9 +59,9 @@ "dev": true }, "@types/node": { - "version": "12.12.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.37.tgz", - "integrity": "sha512-4mXKoDptrXAwZErQHrLzpe0FN/0Wmf5JRniSVIdwUrtDf9wnmEV1teCNLBo/TwuXhkK/bVegoEn/wmb+x0AuPg==", + "version": "12.12.62", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.62.tgz", + "integrity": "sha512-qAfo81CsD7yQIM9mVyh6B/U47li5g7cfpVQEDMfQeF8pSZVwzbhwU3crc0qG4DmpsebpJPR49AKOExQyJ05Cpg==", "dev": true }, "@types/openurl": { @@ -81,9 +74,9 @@ } }, "@types/vscode": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.44.0.tgz", - "integrity": "sha512-WJZtZlinE3meRdH+I7wTsIhpz/GLhqEQwmPGeh4s1irWLwMzCeTV8WZ+pgPTwrDXoafVUWwo1LiZ9HJVHFlJSQ==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.49.0.tgz", + "integrity": "sha512-wfNQmLmm1VdMBr6iuNdprWmC1YdrgZ9dQzadv+l2eSjJlElOdJw8OTm4RU4oGTBcfvG6RZI2jOcppkdSS18mZw==", "dev": true }, "agent-base": { @@ -805,9 +798,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.18.tgz", - "integrity": "sha512-9luZr/BZ2QeU6tO2uG8N2aZpVSli4TSAOAqFOyTO51AJcD9P99c0K1h6dD6r6qo5dyT44BR5exweOaLLeldTkQ==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -956,9 +949,9 @@ } }, "typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", - "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "dev": true }, "underscore": { @@ -967,9 +960,9 @@ "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" }, "vscode-test": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.3.0.tgz", - "integrity": "sha512-LddukcBiSU2FVTDr3c1D8lwkiOvwlJdDL2hqVbn6gIz+rpTqUCkMZSKYm94Y1v0WXlHSDQBsXyY+tchWQgGVsw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.4.0.tgz", + "integrity": "sha512-Jt7HNGvSE0+++Tvtq5wc4hiXLIr2OjDShz/gbAfM/mahQpy4rKBnmOK33D+MR67ATWviQhl+vpmU3p/qwSH/Pg==", "dev": true, "requires": { "http-proxy-agent": "^2.1.0", diff --git a/package.json b/package.json index 0c6026d..34cad31 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "pubspec-assist", "displayName": "Pubspec Assist", "description": "Easily add and update dependencies to your Dart and Flutter project.", - "version": "2.0.0", + "version": "2.1.0", "publisher": "jeroen-meijer", "author": { "name": "Jeroen Meijer", @@ -52,6 +52,11 @@ "command": "pubspec-assist.addDevDependency", "title": "Add/update dev dependency", "category": "Pubspec Assist" + }, + { + "command": "pubspec-assist.sortAllDependencies", + "title": "Sort all dependencies", + "category": "Pubspec Assist" } ], "configuration": [ @@ -64,6 +69,11 @@ "default": true, "description": "If a package with a very close match to your search is found, add it to the pubspec file automatically." }, + "pubspec-assist.sortDependencies": { + "type": "boolean", + "default": true, + "description": "Sort all dependencies and dev_dependencies after adding/updating a dependency using the new YAML parser." + }, "pubspec-assist.useCaretSyntax": { "type": "boolean", "default": true, @@ -87,18 +97,18 @@ "test": "node ./out/test/index.js" }, "devDependencies": { - "@types/glob": "^7.1.1", - "@types/js-yaml": "^3.12.3", + "@types/glob": "^7.1.3", + "@types/js-yaml": "^3.12.5", "@types/mocha": "^5.2.6", - "@types/node": "^12.12.0", + "@types/node": "^12.12.62", "@types/openurl": "^1.0.0", - "@types/vscode": "^1.32.0", + "@types/vscode": "^1.49.0", "glob": "^7.1.4", "mocha": "^6.1.4", - "source-map-support": "^0.5.12", + "source-map-support": "^0.5.19", "tslint": "^5.19.0", - "typescript": "^3.8.3", - "vscode-test": "^1.3.0" + "typescript": "^3.9.7", + "vscode-test": "^1.4.0" }, "dependencies": { "fuse-js-latest": "^3.1.0", diff --git a/src/extension.ts b/src/extension.ts index 8aa07b5..4c6b842 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,7 +4,7 @@ import * as vscode from "vscode"; import * as functions from "./functions"; export function activate(context: vscode.ExtensionContext) { - let commands = { + const commands = { addDependencyCommand: vscode.commands.registerCommand( "pubspec-assist.addDependency", (_: vscode.ExtensionContext) => functions.addDependency("dependencies") @@ -14,7 +14,11 @@ export function activate(context: vscode.ExtensionContext) { (_: vscode.ExtensionContext) => functions.addDependency("dev_dependencies") ), - }; + sortAllDependenciesCommand: vscode.commands.registerCommand( + "pubspec-assist.sortAllDependencies", + (_: vscode.ExtensionContext) => functions.sortAllDependencies() + ), + } as const; Object.values(commands).forEach((command) => context.subscriptions.push(command) diff --git a/src/functions/addDependency.ts b/src/functions/addDependency.ts index 6b747b8..346554c 100644 --- a/src/functions/addDependency.ts +++ b/src/functions/addDependency.ts @@ -5,7 +5,6 @@ import * as vscode from "vscode"; import { showError, showCriticalError, showInfo } from "../helper/messaging"; import { PubAPI } from "../model/pubApi"; -import { PubError } from "../model/pubError"; import { PubPackage } from "../model/pubPackage"; import { getValue } from "../helper/getValue"; import { DependencyType } from "../model/dependencyType"; @@ -13,6 +12,15 @@ import { PubspecContext } from "../model/pubspecContext"; import { LabelIcon } from "../helper/labelIcon"; import { getSettings } from "../helper/getSettings"; import * as YAML from "yaml"; +import { YAMLMap } from "yaml/types"; +import { sortDependencies } from "../helper/sortDependencies"; +import { getFileContext } from "../helper/getFileContext"; +import { getPubspecText } from "../helper/getPubspecText"; +import { formatIfOpened } from "../helper/formatIfOpened"; + +export type PubspecParserResult = + | { success: true; insertionMethod: InsertionMethod; result: string } + | { success: false; error: string }; export enum InsertionMethod { ADD = "Added", @@ -30,7 +38,7 @@ export async function addDependency(dependencyType: DependencyType) { if (!context.openInEditor && !fs.existsSync(context.path)) { showError( - new PubError( + new Error( "Pubspec file not found in workspace root. " + "Open the pubspec file you would like to edit and try again." ) @@ -86,32 +94,34 @@ export async function addDependency(dependencyType: DependencyType) { api.getPackage(chosenPackageString) ); + gettingPackageMessage.dispose(); + if (!chosenPackageResponse) { return; } - gettingPackageMessage.dispose(); - const chosenPackage = chosenPackageResponse.result; try { - const preserveNewline = checkNewlineAtEndOfFile(context); formatIfOpened(context); const pubspecString = getPubspecText(context); - const parser = context.settings.useLegacyParser - ? addDependencyByText - : addDependencyByObject; - - const modifiedPubspec = parser({ + const pubspecParserResult = addDependencyToYamlString({ context, pubspecString, newPackage: chosenPackage, }); - const newPubspecString = modifiedPubspec.result.concat( - preserveNewline ? "\n" : "" - ); + if (!pubspecParserResult.success) { + showError(Error(pubspecParserResult.error)); + return; + } + + let newPubspecString = pubspecParserResult.result; + + if (context.settings.sortDependencies) { + newPubspecString = sortDependencies(newPubspecString); + } if (context.openInEditor) { const originalLines = pubspecString.split("\n"); @@ -134,133 +144,18 @@ export async function addDependency(dependencyType: DependencyType) { formatIfOpened(context); showInfo( - `${modifiedPubspec.insertionMethod.toString()} '${ + `${pubspecParserResult.insertionMethod.toString()} '${ chosenPackage.name - }' (version ${chosenPackage.latestVersion}).` + }' (version ${chosenPackage.latestVersion})${ + !context.settings.sortDependencies ? "" : " and sorted file" + }.` ); } catch (error) { showCriticalError(error); } } -function getFileContext() { - const pubspecIsOpen = pubspecFileIsOpen(); - const pubspecPath = pubspecIsOpen - ? vscode.window.activeTextEditor!.document.uri.fsPath - : `${vscode.workspace.rootPath}/pubspec.yaml`; - - return { - openInEditor: pubspecIsOpen, - path: pubspecPath, - }; -} - -function getPubspecText(context: PubspecContext): string { - return context.openInEditor - ? vscode.window.activeTextEditor!.document.getText() - : fs.readFileSync(context.path, "utf8"); -} - -function checkNewlineAtEndOfFile(context: PubspecContext): boolean { - return getPubspecText(context).substr(-1) === "\n"; -} - -function pubspecFileIsOpen() { - return ( - (vscode.window.activeTextEditor && - (vscode.window.activeTextEditor.document.fileName.endsWith( - "pubspec.yaml" - ) || - vscode.window.activeTextEditor.document.fileName.endsWith( - "pubspec.yml" - ))) || - false - ); -} - -export function addDependencyByText({ - context, - pubspecString, - newPackage, -}: { - context: PubspecContext; - pubspecString: string; - newPackage: PubPackage; -}): { insertionMethod: InsertionMethod; result: string } { - let insertionMethod = InsertionMethod.ADD; - - let lines = pubspecString.split("\n"); - - let dependencyTypeQuery = `${context.dependencyType}:`; - - let dependencyLineIndex = lines.findIndex( - (line) => line.trim() === dependencyTypeQuery - ); - - if (dependencyLineIndex === -1) { - lines.push(dependencyTypeQuery); - dependencyLineIndex = lines.length - 1; - } - - if (dependencyLineIndex === lines.length - 1) { - lines.push(""); - } - - const dependencyString = `${newPackage.name}: ${ - context.settings.useCaretSyntax ? "^" : "" - }${newPackage.latestVersion}`; - - const existingPackageLineIndex = lines.findIndex((line) => { - if (!line.includes(":")) { - return false; - } - - const sanitizedLine: string = line.trim(); - const colonIndex: number = sanitizedLine.indexOf(":"); - const potentialMatch = sanitizedLine.substring(0, colonIndex); - - return potentialMatch.trim() === newPackage.name; - }); - if (existingPackageLineIndex !== -1) { - const originalLine = lines[existingPackageLineIndex]; - - lines[existingPackageLineIndex] = " " + dependencyString; - - if (originalLine.includes("\r")) { - lines[existingPackageLineIndex] += "\r"; - } - if (originalLine.includes("\n")) { - lines[existingPackageLineIndex] += "\n"; - } - - insertionMethod = InsertionMethod.REPLACE; - } else { - for (let i = dependencyLineIndex + 1; i < lines.length; i++) { - if (!lines[i].startsWith(" ") && !lines[i].trim().startsWith("#")) { - lines[i] = " " + dependencyString + "\r\n" + lines[i]; - break; - } - if (i === lines.length - 1) { - if (!lines[i].includes("\r")) { - lines[i] = lines[i] + "\r"; - } - lines.push(" " + dependencyString); - break; - } - } - } - - pubspecString = lines - .join("\n") - .split("\n") - // .map((line, index) => (!(line[0] === " ") ? "\n" + line : line)) - .join("\n") - .trim(); - - return { insertionMethod: insertionMethod, result: pubspecString }; -} - -function addDependencyByObject({ +export function addDependencyToYamlString({ context, pubspecString, newPackage, @@ -268,8 +163,11 @@ function addDependencyByObject({ context: PubspecContext; pubspecString: string; newPackage: PubPackage; -}): { insertionMethod: InsertionMethod; result: string } { - const pubspecDoc = YAML.parseDocument(pubspecString); +}): PubspecParserResult { + const options: YAML.Options = { + schema: "core", + }; + const pubspecDoc = YAML.parseDocument(pubspecString, options); const entryExists = pubspecDoc.hasIn([ context.dependencyType, @@ -284,11 +182,26 @@ function addDependencyByObject({ newPackage.latestVersion }`; - pubspecDoc.setIn([context.dependencyType, newPackage.name], versionString); + const dependencyPath = pubspecDoc.get(context.dependencyType); + const dependencyPathIsEmpty = + dependencyPath === null || dependencyPath === undefined; + const dependencyPathIsYAMLMap = dependencyPath instanceof YAMLMap; + + if (dependencyPathIsEmpty || !dependencyPathIsYAMLMap) { + if (!pubspecDoc.contents) { + pubspecDoc.contents = new YAMLMap(); + } + + pubspecDoc.set(context.dependencyType, { + [newPackage.name]: versionString, + }); + } else { + (dependencyPath as YAMLMap).set(newPackage.name, versionString); + } const result = pubspecDoc.toString(); - return { insertionMethod, result }; + return { success: true, insertionMethod, result }; } function askPackageName(context: PubspecContext): Thenable { @@ -317,11 +230,3 @@ function selectFrom(options: string[]): Thenable { matchOnDescription: true, }); } - -export function formatIfOpened(context: PubspecContext) { - if (context.openInEditor) { - vscode.commands.executeCommand("editor.action.formatDocument"); - } -} - -export default addDependency; diff --git a/src/functions/index.ts b/src/functions/index.ts index 877b066..2ac890e 100644 --- a/src/functions/index.ts +++ b/src/functions/index.ts @@ -1 +1,2 @@ -export { addDependency } from "./addDependency"; +export * from "./addDependency"; +export * from "./sortAllDependencies"; diff --git a/src/functions/sortAllDependencies.ts b/src/functions/sortAllDependencies.ts new file mode 100644 index 0000000..4140dd1 --- /dev/null +++ b/src/functions/sortAllDependencies.ts @@ -0,0 +1,57 @@ +"use strict"; + +import * as fs from "fs"; +import * as vscode from "vscode"; + +import { showError, showCriticalError } from "../helper/messaging"; +import { PubspecContext } from "../model/pubspecContext"; +import { getSettings } from "../helper/getSettings"; +import { sortDependencies } from "../helper/sortDependencies"; +import { getFileContext } from "../helper/getFileContext"; +import { getPubspecText } from "../helper/getPubspecText"; +import { formatIfOpened } from "../helper/formatIfOpened"; + +export async function sortAllDependencies() { + const context: PubspecContext = { + ...getFileContext(), + settings: getSettings(), + }; + + if (!context.openInEditor && !fs.existsSync(context.path)) { + showError( + new Error( + "Pubspec file not found in workspace root. " + + "Open the pubspec file you would like to sort and try again." + ) + ); + return; + } + + try { + const pubspecString = getPubspecText(context); + + const newPubspecString = sortDependencies(pubspecString); + + if (context.openInEditor) { + const originalLines = pubspecString.split("\n"); + vscode.window.activeTextEditor!.edit((editBuilder) => { + editBuilder.replace( + new vscode.Range( + new vscode.Position(0, 0), + new vscode.Position( + originalLines.length - 1, + originalLines[originalLines.length - 1].length + ) + ), + newPubspecString + ); + }); + } else { + fs.writeFileSync(context.path, newPubspecString, "utf-8"); + } + + formatIfOpened(context); + } catch (error) { + showCriticalError(error); + } +} diff --git a/src/escapeHtml.ts b/src/helper/escapeHtml.ts similarity index 100% rename from src/escapeHtml.ts rename to src/helper/escapeHtml.ts diff --git a/src/helper/formatIfOpened.ts b/src/helper/formatIfOpened.ts new file mode 100644 index 0000000..700928a --- /dev/null +++ b/src/helper/formatIfOpened.ts @@ -0,0 +1,8 @@ +import * as vscode from "vscode"; +import { PubspecContext } from "../model/pubspecContext"; + +export function formatIfOpened(context: PubspecContext) { + if (context.openInEditor) { + vscode.commands.executeCommand("editor.action.formatDocument"); + } +} diff --git a/src/helper/getFileContext.ts b/src/helper/getFileContext.ts new file mode 100644 index 0000000..064597e --- /dev/null +++ b/src/helper/getFileContext.ts @@ -0,0 +1,28 @@ +"use strict"; + +import * as vscode from "vscode"; + +export function getFileContext() { + const pubspecIsOpen = pubspecFileIsOpen(); + const pubspecPath = pubspecIsOpen + ? vscode.window.activeTextEditor!.document.uri.fsPath + : `${vscode.workspace.rootPath}/pubspec.yaml`; + + return { + openInEditor: pubspecIsOpen, + path: pubspecPath, + }; +} + +function pubspecFileIsOpen() { + return ( + (vscode.window.activeTextEditor && + (vscode.window.activeTextEditor.document.fileName.endsWith( + "pubspec.yaml" + ) || + vscode.window.activeTextEditor.document.fileName.endsWith( + "pubspec.yml" + ))) || + false + ); +} diff --git a/src/helper/getPubspecText.ts b/src/helper/getPubspecText.ts new file mode 100644 index 0000000..66c7a9a --- /dev/null +++ b/src/helper/getPubspecText.ts @@ -0,0 +1,9 @@ +import * as fs from "fs"; +import * as vscode from "vscode"; +import { PubspecContext } from "../model/pubspecContext"; + +export function getPubspecText(context: PubspecContext): string { + return context.openInEditor + ? vscode.window.activeTextEditor!.document.getText() + : fs.readFileSync(context.path, "utf8"); +} diff --git a/src/helper/getSettings.ts b/src/helper/getSettings.ts index 471a995..85126a0 100644 --- a/src/helper/getSettings.ts +++ b/src/helper/getSettings.ts @@ -4,6 +4,7 @@ export type Settings = { autoAddPackage: boolean; useCaretSyntax: boolean; useLegacyParser: boolean; + sortDependencies: boolean; }; export const getSettings = () => { @@ -13,6 +14,7 @@ export const getSettings = () => { return { autoAddPackage: getSettingByKey("autoAddPackage") ?? true, useCaretSyntax: getSettingByKey("useCaretSyntax") ?? true, + sortDependencies: getSettingByKey("sortDependencies") ?? false, useLegacyParser: getSettingByKey("useLegacyParser") ?? false, }; }; diff --git a/src/helper/messaging.ts b/src/helper/messaging.ts index 16cbd9f..46bb9f2 100644 --- a/src/helper/messaging.ts +++ b/src/helper/messaging.ts @@ -29,20 +29,20 @@ export function showError(error: Error, isCritical: boolean = false): void { if (!isCritical) { message += error.message; vscode.window.showErrorMessage(message); - return; - } - message += `A critical error has occurred.\n + } else { + message += `A critical error has occurred.\n If this happens again, please report it.\n\n Error message: ${error.message}`; - vscode.window - .showErrorMessage(message, {}, ...criticalErrorOptions) - .then((option?: vscode.MessageItem) => { - if (option) { - handleErrorOptionResponse(option.title, error); - } - }); + vscode.window + .showErrorMessage(message, {}, ...criticalErrorOptions) + .then((option?: vscode.MessageItem) => { + if (option) { + handleErrorOptionResponse(option.title, error); + } + }); + } } export async function showRetryableError(error: Error): Promise { diff --git a/src/helper/sortDependencies.ts b/src/helper/sortDependencies.ts new file mode 100644 index 0000000..3e35e22 --- /dev/null +++ b/src/helper/sortDependencies.ts @@ -0,0 +1,61 @@ +import * as YAML from "yaml"; +import { Pair, Scalar, YAMLMap } from "yaml/types"; +import { dependencyTypes } from "../model/dependencyType"; + +export function sortDependencies(pubspecString: string) { + const options: YAML.Options = { + schema: "core", + }; + const pubspecDoc = YAML.parseDocument(pubspecString, options); + + for (const dependencyType of dependencyTypes) { + const dependencyPath = pubspecDoc.get(dependencyType) as + | null + | undefined + | YAMLMap; + + const dependencyPathIsMap = dependencyPath instanceof YAMLMap; + + if ( + dependencyPath === null || + dependencyPath === undefined || + !dependencyPathIsMap + ) { + continue; + } + + const sortByKey = (a: Pair, b: Pair) => + (a.key as Scalar).value < (b.key as Scalar).value ? -1 : 1; + const containsKey = (key: string) => (item: Pair) => + item.value.type === "MAP" && + item.value.items.some((item: Pair) => item.key.value === key); + + const sortedItems = (dependencyPath.items as Pair[]).sort(sortByKey); + + const sortedItemsByImportType = { + sdk: sortedItems.filter(containsKey("sdk")), + path: sortedItems.filter(containsKey("path")), + git: sortedItems.filter(containsKey("git")), + hosted: sortedItems.filter(containsKey("hosted")), + }; + + const newDependencyMap = new YAMLMap(); + for (const item of [ + ...sortedItemsByImportType.sdk, + ...sortedItemsByImportType.path, + ...sortedItemsByImportType.git, + ...sortedItemsByImportType.hosted, + ...sortedItems, + ]) { + if (!newDependencyMap.has(item.key)) { + newDependencyMap.add(item); + } + } + + pubspecDoc.set(dependencyType, newDependencyMap); + } + + return pubspecDoc.toString(); +} + +export default sortDependencies; diff --git a/src/model/dependencyType.ts b/src/model/dependencyType.ts index 0cf2481..ae7b4ee 100644 --- a/src/model/dependencyType.ts +++ b/src/model/dependencyType.ts @@ -1 +1,5 @@ export type DependencyType = "dependencies" | "dev_dependencies"; +export const dependencyTypes: readonly DependencyType[] = [ + "dependencies", + "dev_dependencies", +] as const; diff --git a/src/model/pubApi.ts b/src/model/pubApi.ts index 855d81e..80b8532 100644 --- a/src/model/pubApi.ts +++ b/src/model/pubApi.ts @@ -3,7 +3,7 @@ import "./pubPackage"; import * as rm from "typed-rest-client/RestClient"; import * as Fuse from "fuse-js-latest"; -import { escapeHtml } from "../escapeHtml"; +import { escapeHtml } from "../helper/escapeHtml"; import { PubPackage } from "./pubPackage"; import { PubPackageSearch } from "./pubPackageSearch"; import { PubPage } from "./pubPage"; diff --git a/src/model/pubspecContext.ts b/src/model/pubspecContext.ts index f07bf7e..27a4eac 100644 --- a/src/model/pubspecContext.ts +++ b/src/model/pubspecContext.ts @@ -4,12 +4,12 @@ import { Settings } from "../helper/getSettings"; export type PubspecContext = | { settings: Settings; - dependencyType: DependencyType; + dependencyType?: DependencyType; openInEditor: true; } | { settings: Settings; - dependencyType: DependencyType; + dependencyType?: DependencyType; openInEditor: false; path: string; }; diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index e0f039a..f8cf531 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -1,7 +1,7 @@ import * as assert from "assert"; import * as fs from "fs"; -import { addDependencyByText } from "../functions/addDependency"; +import { addDependencyToYamlString } from "../functions/addDependency"; import { pubspecMockData } from "./pubspecMockData"; import { PubspecMockTestCase } from "./pubspecMockTestCase"; import { @@ -25,19 +25,20 @@ suite("Extension: Dependency Adding Tests", function () { for (let testCase of testCases) { for (let pubspecMock of testCase.mocks) { test(`'${testCase.pubPackage.name}' (${testCase.pubPackage.latestVersion}) -> '${pubspecMock.name}'`, function () { - const result: string = addDependencyByText({ + const result = addDependencyToYamlString({ context: { openInEditor: true, dependencyType: "dependencies", settings: { autoAddPackage: true, useCaretSyntax: true, + sortDependencies: false, useLegacyParser: false, - } + }, }, pubspecString: pubspecMock.source, newPackage: testCase.pubPackage, - }).result; + }); writeLog( "targets.yaml", diff --git a/src/test/pubspecMockData.ts b/src/test/pubspecMockData.ts index ef2c89f..4d64ef3 100644 --- a/src/test/pubspecMockData.ts +++ b/src/test/pubspecMockData.ts @@ -52,4 +52,4 @@ export const pubspecMockData: any = [ }, ], }, -]; +] as const;