From ab8a7cf86d18d7b6467adf62fd080dd1ca2733d4 Mon Sep 17 00:00:00 2001 From: Ofer Belinsky Date: Sun, 5 Aug 2018 22:33:10 +0300 Subject: [PATCH] feat: "Render Conditionally" command for JSX (#16) --- src/code-actions.ts | 23 +++++++++++++++++++++-- src/editor.ts | 4 ++++ src/extension.ts | 7 ++++++- src/modules/jsx.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/code-actions.ts b/src/code-actions.ts index cf1ee60..a10e5c1 100644 --- a/src/code-actions.ts +++ b/src/code-actions.ts @@ -1,12 +1,13 @@ +import { SnippetString } from 'vscode'; import { showDirectoryPicker } from "./directories-picker"; import { showFilePicker } from "./file-picker"; -import { activeEditor, selectedText, activeFileName, openFile, selectedTextStart, selectedTextEnd, showErrorMessage, showInformationMessage } from "./editor"; +import { activeEditor, selectedText, activeFileName, openFile, selectedTextStart, selectedTextEnd, showErrorMessage, showInformationMessage, allText } from "./editor"; import { statelessToStateful } from "./modules/statless-to-stateful"; import { statefulToStateless } from './modules/stateful-to-stateless' import { shouldSwitchToTarget, shouldBeConsideredJsFiles } from "./settings"; import { replaceTextInFile, appendTextToFile, prependTextToFile, removeContentFromFileAtLineAndColumn } from "./file-system"; import { getIdentifier, generateImportStatementFromFile, transformJSIntoExportExpressions } from "./parsing"; -import { createComponentInstance, wrapWithComponent } from "./modules/jsx"; +import { createComponentInstance, wrapWithComponent, isRangeContainedInJSXExpression, isJSXExpression } from "./modules/jsx"; import * as relative from 'relative'; import * as path from 'path'; @@ -31,6 +32,24 @@ export async function extractJSXToComponent() { } } +export async function wrapJSXWithCondition() { + var editor = activeEditor(); + if (!editor) { + return; // No open text editor + } + + try { + const selText = selectedText(); + const isParentJSXExpression = isRangeContainedInJSXExpression(allText(), selectedTextStart(), selectedTextEnd()); + const conditionalJSX = isJSXExpression(selText) ? selText : `<>${selText}`; + const snippetInnerText = `\n$\{1:true\}\n? ${conditionalJSX}\n: $\{2:null\}\n`; + const snippetText = isParentJSXExpression ? `{${snippetInnerText}}` : `(${snippetInnerText})`; + await editor.insertSnippet(new SnippetString(snippetText)); + } catch (e) { + handleError(e); + } +} + export async function extractToFile() { var editor = activeEditor(); if (!editor) { diff --git a/src/editor.ts b/src/editor.ts index 7ef3116..8019108 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -44,6 +44,10 @@ export function selectedText() { return editor.document.getText(selection); } +export function allText() { + const editor = vscode.window.activeTextEditor; + return editor.document.getText(); +} export function showInputBox(defaultValue, placeHolder) { return vscode.window.showInputBox({ diff --git a/src/extension.ts b/src/extension.ts index 14655ae..0d0fbcd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,4 @@ -import { extractToFile, statelessToStatefulComponent, statefulToStatelessComponent, extractJSXToComponent } from './code-actions'; +import { extractToFile, statelessToStatefulComponent, statefulToStatelessComponent, extractJSXToComponent, wrapJSXWithCondition } from './code-actions'; 'use strict'; import * as vscode from 'vscode'; @@ -19,6 +19,9 @@ export class CompleteActionProvider implements vscode.CodeActionProvider { return [{ command: 'extension.glean.react.extract-component', title: 'Extract Component' + }, { + command: 'extension.glean.react.render-conditionally', + title: 'Render Conditionally' }]; } @@ -51,6 +54,8 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('extension.glean.react.extract-component', extractJSXToComponent); + vscode.commands.registerCommand('extension.glean.react.render-conditionally', wrapJSXWithCondition); + vscode.commands.registerCommand('extension.glean.react.stateless-to-stateful', statelessToStatefulComponent); vscode.commands.registerCommand('extension.glean.react.stateful-to-stateless', statefulToStatelessComponent); diff --git a/src/modules/jsx.ts b/src/modules/jsx.ts index 9a86d45..46161f4 100644 --- a/src/modules/jsx.ts +++ b/src/modules/jsx.ts @@ -31,6 +31,46 @@ export function isJSX(code) { return ast && ast.expression && t.isJSX(ast.expression); } +export function isJSXExpression(code) { + try { + const ast = template.ast(code, defaultTemplateOptions); + return ast && ast.expression && t.isJSX(ast.expression); + } catch (e) { + return false; + } +} + +export function isRangeContainedInJSXExpression(code, start, end) { + try { + const ast = codeToAst(code) + const path = findContainerPath(ast, start, end) + return path && t.isJSX(path.node) && t.isExpression(path.node); + } catch (e) { + return false + } +} + +function findContainerPath(ast, start, end) { + let foundPath = null; + const visitor = { + exit(path) { + if (!foundPath && pathContains(path, start, end)) { + foundPath = path + } + } + } + + traverse(ast, visitor); + return foundPath; +} + +function pathContains(path, start, end) { + const pathStart = path.node.loc.start + const pathEnd = path.node.loc.end + return ((pathStart.line < start.line) || (pathStart.line === start.line && pathStart.column < start.character)) + && ((pathEnd.line > end.line) || (pathEnd.line === end.line && pathEnd.column > end.character)) +} + function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); }