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

Feature/plugin schema proposal #185

Draft
wants to merge 2 commits into
base: refactor/preview-source
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added expo-config-plugins-7.2.0.tgz
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
134 changes: 88 additions & 46 deletions src/manifestPluginCompletions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string, boolean>,
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.
Expand All @@ -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.
Expand All @@ -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)}`;
Expand Down
35 changes: 34 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down