diff --git a/expo-config-plugins-7.2.0.tgz b/expo-config-plugins-7.2.0.tgz new file mode 100644 index 00000000..497edbf1 Binary files /dev/null and b/expo-config-plugins-7.2.0.tgz differ diff --git a/package.json b/package.json index 3766c6b0..adf59728 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "install:fixture": "yarn install --cwd ./test/fixture" }, "devDependencies": { - "@expo/config-plugins": "^6.0.2", + "@expo/config-plugins": "file:./expo-config-plugins-7.2.0.tgz", "@expo/json-file": "^8.2.37", "@expo/prebuild-config": "^6.0.1", "@semantic-release/changelog": "^6.0.1", diff --git a/src/manifestPluginCompletions.ts b/src/manifestPluginCompletions.ts index 002b8ab0..7a9e61b0 100644 --- a/src/manifestPluginCompletions.ts +++ b/src/manifestPluginCompletions.ts @@ -4,7 +4,7 @@ import vscode from 'vscode'; import { manifestPattern } from './expo/manifest'; import { PluginInfo, resolveInstalledPluginInfo, resolvePluginInfo } from './expo/plugin'; -import { ExpoProjectCache } from './expo/project'; +import { ExpoProject, ExpoProjectCache } from './expo/project'; import { getManifestFileReferencesExcludedFiles, isManifestFileReferencesEnabled, @@ -51,70 +51,112 @@ export class ManifestPluginCompletionsProvider extends ExpoCompletionsProvider { return []; } - // Abort if we can't locate the cursor, or if the cursor is on a JSON key property - const positionNode = findNodeAtOffset(project.manifest.tree, document.offsetAt(position)); + // Abort if we can't locate the current position within the manifest ast + const positionNode = findPositionNode(project, document, position); if (!positionNode || isKeyNode(positionNode)) return null; - // Abort if the cursor is not in the plugins property - const plugins = findNodeAtLocation(project.manifest.tree, ['plugins']); - const positionInPlugins = plugins && getDocumentRange(document, plugins).contains(position); - if (!positionInPlugins) return null; + // Abort if the current position is not within the `expo.plugins` area of the manifest + const pluginsNode = findManifestPluginsNode(project); + if (!pluginsNode || !getDocumentRange(document, pluginsNode).contains(position)) return null; + // Fetch the basic information of the exact node the user is currently editing + // This determines the type of autocompletion we can provide const positionValue = getNodeValue(positionNode); const positionIsPath = positionValue && positionValue.startsWith('./'); // Create a list of installed Expo plugins when referencing a plugin by package name if (!positionIsPath && !token.isCancellationRequested) { - return createPossibleIncompleteList( - resolveInstalledPluginInfo(project, positionValue, MAX_RESULT).map((plugin) => - createPluginModule(plugin) - ) - ); + return completePluginFromPackages(project, positionValue); } // Create a list of local Expo plugin files when referencing a plugin by path if (positionIsPath && !token.isCancellationRequested) { - const positionDir = getDirectoryPath(positionValue) ?? ''; - const entities = await withCancelToken(token, () => - vscode.workspace.fs.readDirectory(vscode.Uri.file(path.join(project.root, positionDir))) - ); - - return entities - ?.map(([entityName, entityType]) => { - if (fileIsHidden(entityName) || fileIsExcluded(entityName, this.excludedFiles)) { - return null; - } - - if (entityType === vscode.FileType.Directory) { - return createFolder(entityName); - } - - if (path.extname(entityName) === '.js') { - const pluginPath = './' + path.join(positionDir, entityName); - const plugin = resolvePluginInfo(project.root, pluginPath); - if (plugin) { - return createPluginFile(plugin, entityName); - } - } - }) - .filter(truthy); + return completePluginOrFolderFromPath(project, positionValue, this.excludedFiles, token); } + // Return no completion items if none can be found return null; } } -function createPossibleIncompleteList( - items: vscode.CompletionItem[], - isIncomplete?: boolean -): vscode.CompletionList { - return new vscode.CompletionList( - items, - isIncomplete !== undefined ? isIncomplete : items.length >= MAX_RESULT +/** + * Find the app manifest JSON node for the current position within the document. + * It describes the area the user is currently editing within the file. + */ +function findPositionNode( + project: ExpoProject, + document: vscode.TextDocument, + position: vscode.Position +) { + return findNodeAtOffset(project.manifest!.tree, document.offsetAt(position)); +} + +/** + * Find the app manifest JSON node for the `expo.plugins` definition. + * This contains all the plugin information provided by the user, + * and is the area this completion provider focuses on. + */ +function findManifestPluginsNode(project: ExpoProject) { + return findNodeAtLocation(project.manifest!.tree, ['plugins']); +} + +/** + * Create a list of installed Expo plugins when referencing a plugin by package name. + * These autocompletions can be provided based on the user input: + * - `expo-` -> [expo-camera, expo-updates] + * - `expo-u` -> [expo-updates] + */ +function completePluginFromPackages(project: ExpoProject, userInput: string) { + const infos = resolveInstalledPluginInfo(project, userInput, MAX_RESULT); + const items = infos.map((plugin) => createPluginModuleItem(plugin)); + + return new vscode.CompletionList(items); +} + +/** + * Create a list of local Expo plugin files when referencing a plugin by path. + * These autocompletions can be provided based on the user input: + * - `./` -> [./folder, ./plugin.js] + * - `./folder/` -> [plugin.js] (nested inside ./folder) + */ +async function completePluginOrFolderFromPath( + project: ExpoProject, + userInput: string, + excludedFiles: Record, + token: vscode.CancellationToken +) { + // Find the directory we need to search for plugins + const positionDir = getDirectoryPath(userInput) ?? ''; + // Find all entities within that directory, relative from project root + const entities = await withCancelToken(token, () => + vscode.workspace.fs.readDirectory(vscode.Uri.file(path.join(project.root, positionDir))) ); + + // Generate completion items for each entity + return entities + ?.map(([entityName, entityType]) => { + // Skip hidden or excluded files + if (fileIsHidden(entityName) || fileIsExcluded(entityName, excludedFiles)) { + return null; + } + + // This system does not look ahead inside the folder, so any folder should be a valid completion item + if (entityType === vscode.FileType.Directory) { + return createFolderItem(entityName); + } + + // Limit the expensive plugin resolution to files with the `.js` extension only + if (path.extname(entityName) === '.js') { + // Try to resolve the plugin, if its a valid plugin file, create a completion item + const pluginPath = './' + path.join(positionDir, entityName); + const plugin = resolvePluginInfo(project.root, pluginPath); + if (plugin) return createPluginFileItem(plugin, entityName); + } + }) + .filter(truthy); } -function createPluginModule(plugin: PluginInfo): vscode.CompletionItem { +function createPluginModuleItem(plugin: PluginInfo): vscode.CompletionItem { const item = new vscode.CompletionItem(plugin.pluginReference, vscode.CompletionItemKind.Module); // Sort app.plugin.js plugins higher since we can be sure that they have a valid plugin. @@ -127,7 +169,7 @@ function createPluginModule(plugin: PluginInfo): vscode.CompletionItem { return item; } -function createPluginFile(plugin: PluginInfo, pluginFile: string): vscode.CompletionItem { +function createPluginFileItem(plugin: PluginInfo, pluginFile: string): vscode.CompletionItem { const item = new vscode.CompletionItem(pluginFile, vscode.CompletionItemKind.File); // Sort app.plugin.js plugins higher since we can be sure that they have a valid plugin. @@ -141,7 +183,7 @@ function createPluginFile(plugin: PluginInfo, pluginFile: string): vscode.Comple * Note, this adds a trailing `/` to the folder and triggers the next suggestion automatically. * While this makes it harder to type `./folder`, `./folder/` is a valid shorthand for `./folder/index.js`. */ -function createFolder(folderPath: string): vscode.CompletionItem { +function createFolderItem(folderPath: string): vscode.CompletionItem { const item = new vscode.CompletionItem(folderPath + '/', vscode.CompletionItemKind.Folder); item.sortText = `e_${path.basename(folderPath)}`; diff --git a/yarn.lock b/yarn.lock index 2ccfb92d..4ade7750 100644 --- a/yarn.lock +++ b/yarn.lock @@ -375,7 +375,27 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@expo/config-plugins@^6.0.2", "@expo/config-plugins@~6.0.0": +"@expo/config-plugins@file:./expo-config-plugins-7.2.0.tgz": + version "7.2.0" + resolved "file:./expo-config-plugins-7.2.0.tgz#4bf4ea2f1c41111f612726534fd6d9e4eae61aaf" + dependencies: + "@expo/config-types" "^49.0.0-alpha.1" + "@expo/json-file" "~8.2.37" + "@expo/plist" "^0.0.20" + "@expo/sdk-runtime-versions" "^1.0.0" + "@react-native/normalize-color" "^2.0.0" + chalk "^4.1.2" + debug "^4.3.1" + find-up "~5.0.0" + getenv "^1.0.0" + glob "7.1.6" + resolve-from "^5.0.0" + semver "^7.3.5" + slash "^3.0.0" + xcode "^3.0.1" + xml2js "0.6.0" + +"@expo/config-plugins@~6.0.0": version "6.0.2" resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-6.0.2.tgz#cf07319515022ba94d9aa9fa30e0cff43a14256f" integrity sha512-Cn01fXMHwjU042EgO9oO3Mna0o/UCrW91MQLMbJa4pXM41CYGjNgVy1EVXiuRRx/upegHhvltBw5D+JaUm8aZQ== @@ -401,6 +421,11 @@ resolved "https://registry.npmjs.org/@expo/config-types/-/config-types-48.0.0.tgz#15a46921565ffeda3c3ba010701398f05193d5b3" integrity sha512-DwyV4jTy/+cLzXGAo1xftS6mVlSiLIWZjl9DjTCLPFVgNYQxnh7htPilRv4rBhiNs7KaznWqKU70+4zQoKVT9A== +"@expo/config-types@^49.0.0-alpha.1": + version "49.0.0-alpha.1" + resolved "https://registry.npmjs.org/@expo/config-types/-/config-types-49.0.0-alpha.1.tgz#fbbe8a10c4577dc16856d48c96b3ce667f5a845b" + integrity sha512-zNqLOEEuVWmsc/Igi2+f1oB0TH2xiqihxjAD/URO2l/r3gYGfaTTw1pP2hn2MACCynxQxLKVL/j77YCr0N346A== + "@expo/config@~8.0.0": version "8.0.2" resolved "https://registry.npmjs.org/@expo/config/-/config-8.0.2.tgz#53ecfa9bafc97b990ff9e34e210205b0e3f05751" @@ -8773,6 +8798,14 @@ xml2js@0.4.23, xml2js@^0.4.23: sax ">=0.6.0" xmlbuilder "~11.0.0" +xml2js@0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282" + integrity sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + xmlbuilder@^14.0.0: version "14.0.0" resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz#876b5aec4f05ffd5feb97b0a871c855d16fbeb8c"