DocsGeneratorContextual

Contextual Generators

Provide suggestions that require context from other flags or options in the current edit buffer.

Use a contextual generator when you want to run scripts dynamically based on parts of the command that the user has entered.

For example, Heroku requires the function takes a parameter, context, which includes an array of strings broken down into components. The strings can then be used as values to pass to the script you'd like to return.

Note: The context array will group quoted components together. For example, when running touch "new file", the value of context is ["touch", "new file"].


See "Tokenizer" for more details on how the edit buffer is turned into the context array.

Reimplementing the filepath Generator

Goals

  • Write a generator that reads the files and folders in a given directory, starting with the user's current directory.

  • Update the list of suggestions any time the user 'enters' a new folder.

  • Output the full filepath in order to use the Icon API to access the native macOS file icons.

The command we'll use looks like this:

ls -1AF /directory/to/show/suggestions/for/ | xargs -I '{}' echo $PWD/{}

Since we need to update the path that is passed in to ls as the user navigates their file system, we can't define script as a static string.

Instead, we need to run an updated version of script whenever the user enters a new folder.

Triggering the generator and filtering suggestions

We can do this by creating a trigger on the / character. This will cause the generator to rerun it's script whenever a slash is inserted or deleted from the edit buffer.

trigger: "/",
filterTerm: "/"

We also set filterTerm equal to / , so that Fig will only filter suggestions using the final component of the path.

For example, if the user has typed:

$ cd ~/Desktop/hello

We only want to use the string "hello" for filtering over the list of file & folder suggestions.

See "Triggers & Filtering" for more information.

Writing a contextual script function

The script function will first run when the user adds a space after cd.

$ cd |

In this case the context array will contain ["cd", ""]. We'll start by storing the argument in variable path and execute the command.

script: function(context) {
  let path = context[context.length - 1]

  return `ls -1AF ${path} | xargs -I '{}' echo $PWD/{}``
}

Invariant: When the script function is called, the last component of context will be the current argument.

While the first time that the script function is run, the arg will be empty, this isn't always the case.

$ cd ~/Desktop/|

If the user presses the Delete key now, the script function will be triggered again and this time the context array will contain ["cd", "~/Desktop"]. If we use our current function, which blindly passes in the argument as the path, we will list the files and folders on the Desktop, rather than the ones in the user's home directory.

To fix this, we need to check if the path contains a slash and, if it does, drop the last component of the path. Otherwise, we should just list the files and folders in the current directory.

if (path.includes('/')) {
  let components = path.split('/')
  components.pop()
  path = components.join('/')
} else {
  path = '.'
}

Finally, let's escape any spaces that appear in the path.

path = path.split(" ").join("\\ ")

Here is the final result:

 script: function(context) {
      let path = context[context.length - 1]

      if (path.includes('/')) {
        let components = path.split('/')
        components.pop()
        path = components.join('/')
      } else {
        path = '.'
      }

      // escape spaces in path
      path = path.split(" ").join("\\ ")

      // output full filepath for each item in `path` folder
      return `ls -1AF ${path} | xargs -I '{}' echo $PWD/{}`
    }

Postprocessing the list of files and folders

The last step is writing a function to take the output of ls and convert it to an array of Suggestion objects.

 postProcess: function(files) {
  return files.split('\n').map(path => {
    let components = path.split('/')
    let filename = components[components.length - 1]

    let isFolder = path.endsWith('/')
    if (isFolder) {
      filename = components[components.length - 2] + '/'
    }

    return {
      name: filename,
      icon: 'fig://' + path,
      description: isFolder ? 'folder' : 'file'
    }
  })

In this postProcess function, we split the raw output on new lines and then map over it, making slight tweaks to the Suggestion object depending on whether the item is a file or a folder.

To set the icon, we pass the full path of the file to the fig:// URL scheme, which returns the native filesystem icon associated with it.

See " Icon API" for more details.

Full Code

const filepathGenerator: Fig.Generator = {
      script: function(context) {
        let path = context[context.length - 1]

        if (path.includes('/')) {
          let components = path.split('/')
          components.pop()
          path = components.join('/')
        } else {
          path = '.'
        }

        // escape spaces in path
        path = path.split(" ").join("\\")

        // output full filepath for each item in `path` folder
        return `ls -1AF ${path} | xargs -I '{}' echo $PWD/{} `
      },

      postProcess: function(files) {
        return files.split('\n').map(path => {
          let components = path.split('/')
          let filename = components[components.length - 1]

          let isFolder = path.endsWith('/')
          if (isFolder) {
            filename = components[components.length - 2] + '/'
          }

          return {
            name: filename,
            icon: 'fig://' + path,
            description: isFolder ? 'folder' : 'file'
          }
        })
      },


      trigger: "/",
      filterTerm: "/"

}