npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

typescript-plugins-text-based-user-interaction

v0.0.17

Published

Helpers for TypeScript Language Service Plugins that wants to interact with the user via the source file itself

Downloads

52

Readme

typescript-plugins-text-based-user-interaction

Helpers for TypeScript Language Service Plugins that wants to interact with the user via the source file itself

  • If you are developing TypeScript plugins that require to interact with the user - but want to be portable, editor/IDE agnostic you can use this library to interact with the user via expressions in the source file itself.
  • default implementation for my plugins when need to inquire the user - not good looking but very powerful, portable, and cammon, here we are inquiring DEVELOPERS trying to refactor their source code, not Mickey Mouse!
  • almost automatic implementation of a plugin from a config object
  • this way I can focus on implementing the plugin / refactor itself and have a working / flexible solution and leave the nice UI work to someone else or when I have time for that - now the real need is ts refactors!
  • just defining a config object it practically generates a working plugin that will suggest autocomplete "templates" and prompt user with function calls that he can modify and call refactors upon "to enter the data" (see demos and examples below)
  • right now very dependant on TypeScript Language Service API

Demo

Moving and renaming a file in Visual studio Code Editor:

  • Moving and renaming a file in Visual studio Code Editor

Editor agnostic!. See the same demo but in Atom editor:

  • Moving and renaming a file in Atom editor: Editor

  • Another demo, this time inquiring two arguments: Moving an interface to another file

Ideas

  • we have many ideas on how the interaction could be right now is like that, with a free-signature function call - so user can model it at piaccere. But this could be improved - for example, instead of a function call, user could be promopted like in a form and we could json-schema validate / hint/autocomplete/etc.
  • Probably the best idea would be maintaining the protocol visual-metaphor-agnostic (perhaps json-schema is a good idea) and then implement different metaphor that user can choose to use by config and others contribute with new ones.

TODO:

  • documentation, examples
  • Big TODO: Language server protocol instead just TLS. implement this 100% there and you have plugins that will work on any ed.
  • also related to previous - could we be language (ts) agnostic and provide the same capability to any LSP enable language not only to TS ?

Example

The following example are fragments of reorder signature parameters plugin. This plugin objective is to change the order of parameters on a given function signature or call. We need to know from the user, interactively, what's the function he want's to change its signature and how the parameters will be re-ordered in the signature. See the gif (TODO) for how the experience is. Basically the user can create a "snippet" of an special function call, modify the call (entering the data) and then applying a refactor to let us know the input.

// here we import the text-based-ui tool
import { Action, create } from 'typescript-plugins-text-based-user-interaction';
import * as ts from 'typescript';
import { getPluginCreate, LanguageServiceOptionals, createSimpleASTProject } from 'typescript-plugin-util';
import { SourceFile, TypeGuards, SignaturedDeclaration, NamedNode, Node } from 'ts-simple-ast';
import { reorderParameters } from './reorderParams';

const PLUGIN_NAME = 'typescript-plugin-function-signature-refactors'
const REFACTOR_ACTION_NAME = `${PLUGIN_NAME}-refactor-action`

// here we create the tool providing a configuration object that define "actions". In this case a simple one
const interactionTool = create({
  actions: [
    {

      // these are the names of the parameters user wil have to modify in function call `reorderParams()`. After user apply the final 
      // refactor calling  interactionTool.getApplicableRefactors or tool.findActions(sourceFile.getText()) will generate this object
      args: ['name', 'reorder'],

      // dynamically create the text for function call `reorderParams()` snippet that user will need to modify. In this case we want 
      // to print the closest function-like name in the ASt to the current user's position and a comment with help
      snippet: (fileName: string, position: number): string | undefined => {
        let func = lookForClosestFunction(fileName, position)
        if (!func || func.parameters && func.parameters.length <= 1) {
          return
        }
        const reorder = []
        for (let i = 0; i < func.parameters.length; i++) {
          reorder.push(func.parameters.length - i - 1)
        }
        return `reorderParams("${func.name.getText()}", [${reorder.join(', ')}])
/* Help: The second argument is the new order of parameters. \`param[N] === M\` means move the Nth parameter to the Mth position. 
For example, \`[2, 3]\` means put the first parameter in the third (2) place and the second parameter in the fourth (3) place. 
Other parameters will adjust their positions to comply with this, for example, the third parameter will move to the first place and so on. */`
      },

      // a special prefix our comments will start with so we can differentiate them from others and suggest our special 
      // refactors when the user is there. 
      prefix: '&%&%',
      
      // we are able to enrich the autocomplete suggestions with extra text
      nameExtra: (fileName: string, position: number) => {
        const func = getFunction(fileName, position)
        return func ? `of ${func.name.getText()}` : ''
      },
      // the message in the final refactor suggestion
      print: action => `Reorder parameters of "${action.args.name}"`,
    }
  ]
})

let selectedAction:Action
// we delegate all the work in interactionTool.getApplicableRefactors - will take care of displaying refactor suggestions if the user is standing in a speciall comment marked with the special preffix 
function getApplicableRefactors(fileName: string, positionOrRange: number | ts.TextRange, userPreferences: ts.UserPreferences):  ts.ApplicableRefactorInfo[] {
  const result = interactionTool.getApplicableRefactors(info, `${PLUGIN_NAME}-refactor`, REFACTOR_ACTION_NAME, fileName, positionOrRange, userPreferences)
  selectedAction =  result.selectedAction // This was the action selected by the user using autosuggestions
  return result.refactors
}

// we delegate most of the work to info.languageService.getEditsForRefactor. At this point user input is available in 
// selectedAction.args.name and selectedAction.args.reorder which we use to implement the actual refactor (implementation not relevant)
function getEditsForRefactor(fileName: string, formatOptions: ts.FormatCodeSettings, positionOrRange: number | ts_module.TextRange, refactorName: string, actionName: string, userPreferences: ts_module.UserPreferences): ts.RefactorEditInfo | undefined {
  const refactors = info.languageService.getEditsForRefactor(fileName, formatOptions, positionOrRange, refactorName, actionName, userPreferences)
  if (actionName !== REFACTOR_ACTION_NAME) {
    return refactors
  }
  const targetFunctionDeclaration = getTargetFunction(sourceFile, positionOrRangeToNumber(positionOrRange), selectedAction.args.name)
  reorderParameters(targetFunctionDeclaration, selectedAction.args.reorder) 
}

// autocompletions! notice how I delegate the work on the tool interactionTool.getCompletionsAtPosition()
function getCompletionsAtPosition(fileName: string, position: number, options: ts_module.GetCompletionsAtPositionOptions | undefined): ts_module.CompletionInfo {
  const prior = info.languageService.getCompletionsAtPosition(fileName, position, options)
  if (prior) {
    prior.entries = prior.entries.concat(interactionTool.getCompletionsAtPosition(fileName, position, options))
  }
  return prior
}


// our plugin definition and registration (using typescript-plugin-util)
let info: ts_module.server.PluginCreateInfo
const pluginDefinition: LanguageServiceOptionals = {
  getApplicableRefactors, getEditsForRefactor, getCompletionsAtPosition
}
export = getPluginCreate(pluginDefinition, (modules, anInfo) => {
  info = anInfo
})

Example (old one)

The following is a fragment of typescript-plugin-move-file typescript plugin which uses this library to ask the user where to move current file or current directory:

The functions getApplicableRefactors, getEditsForRefactor, getCompletionsAtPosition are the ones to be added in the TypeScript Language Service Plugin proxy object. See https://github.com/Microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin for more info about typescript plugins.

import { Action, create } from 'typescript-plugins-text-based-user-interaction';
const PLUGIN_NAME = 'typescript-plugin-move-file'
const REFACTOR_ACTION_NAME = `${PLUGIN_NAME}-refactor-action`

// tool creation. 
const tool = create({
  prefix: '&%&%',
  actions: [
    {
      name: 'moveThisFileTo',
      args: ['dest'],
      print: (action) => `Move this file to ${action.args.dest}`,
      snippet: 'moveThisFileTo(\'../newName.ts\')'
    },
    {
      name: 'moveThisFolderTo',
      args: ['dest'],
      print: (action) =>`Move this folder to ${action.args.dest}`,
      snippet: 'moveThisFileTo(\'../newName\')'
    }
  ]
})

let selectedAction: Action

function getApplicableRefactors(fileName: string, positionOrRange: number | ts.TextRange)
  : ts.ApplicableRefactorInfo[] {
  const refactors = info.languageService.getApplicableRefactors(fileName, positionOrRange) || []
  const sourceFile = info.languageService.getProgram().getSourceFile(fileName)
  // we ask the tool to search in current file text if there are any expressions like ` // &%&% moveThisFileTo(...)` or `// &%&% moveThisFolderTo(..)`
  const actions = tool.findActions(sourceFile.getText())
  if (!actions || actions.length === 0) {
    return refactors
  }
  selectedAction = actions[0]
  // selectedAction is the action defined by the user, for example, selectedAction.args.dest is the 
  // destination file specified by the user in a comment like: 
  // `// &%&% moveThisFileTo('../units/Warrior.ts') `. 
  // In the next statement, we use selectedAction.print to print a description of the action 
  // (provided by the user in the config): 
  refactors.push({
    name: `${PLUGIN_NAME}-refactor-info`,
    description: 'move-file-action',
    actions: [{ 
      name: REFACTOR_ACTION_NAME + '-' + selectedAction.name, 
      description: selectedAction.print(selectedAction) 
    }]
  })
  return refactors
}

function getEditsForRefactor(fileName: string, formatOptions: ts.FormatCodeSettings,
  positionOrRange: number | ts_module.TextRange, refactorName: string,
  actionName: string): ts.RefactorEditInfo | undefined {
  const refactors = info.languageService.getEditsForRefactor(fileName, formatOptions, positionOrRange, refactorName, actionName)
  if (!actionName.startsWith(REFACTOR_ACTION_NAME) || !selectedAction) {
    return refactors
  }
  // now we can implement the refactor since we have input information from the user in selectedAction.args - 
  // particularly in this example args.dest - the path where the user want's to move the file or folder
  // whe user apply the refactor this object is automatically feeded with this input in args prop
  // ....
}

// We also ask the tool for completions at position so when user starts writing "refactor" it will be offered with snippets defined in the config for each type of action
function getCompletionsAtPosition (fileName:string, position: number, options: ts_module.GetCompletionsAtPositionOptions | undefined): ts_module.CompletionInfo {
  const prior = info.languageService.getCompletionsAtPosition(fileName, position, options);
  prior.entries = prior.entries.concat(tool.getCompletionsAtPosition(fileName,position, options))
  return prior;
};

TODO / Roadmap

Changelog

0.0.5

  • support for multi-line action declarations.
  • support for dinamic autocomplete suggestions