Best practices for porting extensions from other editors?

Anyone have any best practices for converting extensions from other editors? I don’t know enough about the problem domain to get started on my own, but I can do grunt work to help port things given some starting point.

1 Like

By no means a proper port, but I hacked this nugget together last night so I could reanimate a few Textmate scripts. Converting Nova’s Range coordinate system to a row-column coordinate system is essentially this from @apexskier’s TypeScript extension. (@logan, some built-in Range ↔︎ row-column conversion would be a spectacular feature. It seems that just about every extension I write needs that to interact with things outside of Nova.)

// Compute TM_* environment variables to run scripts written for
// Textmate bundles.
// https://macromates.com/manual/en/environment_variables
function tmVars(editor) {
  let vars = {};

  // shell commands which are (indirectly) triggered from a bundle item
  // which could be a Command, Drag Command, Macro, or Snippet) will have
  // this variable pointing to the Support folder of the bundle that ran
  // the item, if such a folder exists. In addition, $TM_BUNDLE_SUPPORT/bin
  // will be added to the path.
  vars.TM_BUNDLE_SUPPORT = nova.extension.path;
  vars.PATH = [
    nova.environment.PATH,
    nova.path.join(vars.TM_BUNDLE_SUPPORT, "bin"),
  ].join(":");

  // textual content of the current line.
  let clr = editor.getLineRangeForRange(
    new Range(editor.selectedRange.start, editor.selectedRange.start)
  );
  vars.TM_CURRENT_LINE = editor.getTextInRange(clr).replace(/\n$/, "");

  // the word in which the caret is located.
  // vars.TM_CURRENT_WORD = TODO

  if (editor.document.path) {
    // the folder of the current document (may not be set).
    vars.TM_DIRECTORY = nova.path.dirname(editor.document.path);

    // path (including file name) for the current document (may not be set).
    vars.TM_FILEPATH = editor.document.path;
  }

  let l = RangeToLspRange(editor.document, editor.selectedRange);

  // the index in the current line which marks the caret’s location. This index
  // is zero-based and takes the utf-8 encoding of the line (e.g. read as
  // TM_CURRENT_LINE) into account.
  vars.TM_LINE_INDEX = `${l.start.character}`;

  // the carets line position (counting from 1).
  vars.TM_LINE_NUMBER = `${l.start.line + 1}`;

  // the top-level folder in the project drawer (may not be set).
  if (nova.workspace.path) {
    vars.TM_PROJECT_DIRECTORY = nova.workspace.path;
  }

  // the scope that the caret is inside. See scope selectors for information about scopes.
  // vars.TM_SCOPE = TODO

  // vars.TM_SELECTED_FILES = TODO
  // vars.TM_SELECTED_FILE = TODO

  // full content of the selection (may not be set).
  if (editor.selectedText) {
    vars.TM_SELECTED_TEXT = editor.selectedText;
  }

  // this will have the value YES if the user has enabled soft tabs, otherwise it has the value NO.
  vars.TM_SOFT_TABS = editor.softTabs ? "YES" : "NO";

  // the TextMate application bundle contains a support folder with several
  // items which are used by some of the default commands (for example
  // CocoaDialog, Markdown, the SCM commit window, Textile, tidy, etc.).
  vars.TM_SUPPORT_PATH = nova.path.join(
    nova.environment.HOME,
    "Library/Application Support/TextMate/Managed/Bundles/Bundle Support.tmbundle/Support/shared"
  );
  vars.PATH = [
    nova.environment.PATH,
    nova.path.join(vars.TM_SUPPORT_PATH, "bin"),
  ].join(":");

  // the tab size as shown in the status bar.
  vars.TM_TAB_SIZE = `${editor.tabLength}`;

  // console.log(JSON.stringify(vars));

  return vars;
}
3 Likes

+1 to this. I’ve implemented a different form of this in three different places - for LSP, eslint, and jest

Agreed. We are actively looking at implementing line / column access in the extension API—as Nova does track this information internally. I can’t make a guarantee which version it will appear in, but I’m planning soon.

1 Like