Repository: nicoespeon/abracadabra (ts)

Issue:
Extension not working for .vue files
## Is this request related to a problem? Please describe.

The extension does not work for vue files that contain both html/pug and js 

## Describe the solution you'd like

I would like the extension to recognize .vue files as js files


Pull request:
Support .vue files
Resolves #133

This yields interesting refactorings in the source code, but overall it was easy to implement:

![image](https://user-images.githubusercontent.com/1094774/92189629-2894c980-ee2d-11ea-92dd-cd7241f04040.png)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c762fed..0584bf1f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Added
+
+- **Support for `.vue` files**. You can now run automated refactorings within the `<script>` tags of your single-file component ✨
+
 ## [4.7.0] - 2020-08-28 - I like to Move it, Move it 🇲🇬
 
 ### Changed
diff --git a/README.md b/README.md
index 35f68019..67c97a3b 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
 [![Build Status](https://travis-ci.org/nicoespeon/abracadabra.svg?branch=master)](https://travis-ci.org/nicoespeon/abracadabra)
 ![](https://img.shields.io/badge/it%27s-magic-purple.svg)
 
-Abracadabra is a Visual Studio Code extension that brings you automated refactorings for JavaScript and TypeScript.
+Abracadabra is a Visual Studio Code extension that brings you automated refactorings for JavaScript and TypeScript. Works in `.js`, `.jsx`, `.ts`, `.tsx` and `.vue` files.
 
 Our goal is to provide you with easy-to-use, intuitive refactorings. They help you clean the code and understand what's going on.
 
diff --git a/package.json b/package.json
index 4d338183..5a9ed3d0 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,8 @@
     "legacy code",
     "javascript",
     "typescript",
-    "react"
+    "react",
+    "vue"
   ],
   "categories": [
     "Formatters",
@@ -121,7 +122,8 @@
     "onLanguage:javascript",
     "onLanguage:javascriptreact",
     "onLanguage:typescript",
-    "onLanguage:typescriptreact"
+    "onLanguage:typescriptreact",
+    "onLanguage:vue"
   ],
   "contributes": {
     "commands": [
@@ -352,6 +354,10 @@
           "command": "abracadabra.addBracesToArrowFunction",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.addBracesToArrowFunction",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.addBracesToIfStatement",
           "when": "editorLangId == javascript"
@@ -368,6 +374,10 @@
           "command": "abracadabra.addBracesToIfStatement",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.addBracesToIfStatement",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.convertForToForeach",
           "when": "editorLangId == javascript"
@@ -384,6 +394,10 @@
           "command": "abracadabra.convertForToForeach",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.convertForToForeach",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.convertFunctionDeclarationToArrowFunction",
           "when": "editorLangId == javascript"
@@ -400,6 +414,10 @@
           "command": "abracadabra.convertFunctionDeclarationToArrowFunction",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.convertFunctionDeclarationToArrowFunction",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.convertIfElseToTernary",
           "when": "editorLangId == javascript"
@@ -416,6 +434,10 @@
           "command": "abracadabra.convertIfElseToTernary",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.convertIfElseToTernary",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.convertIfElseToSwitch",
           "when": "editorLangId == javascript"
@@ -432,6 +454,10 @@
           "command": "abracadabra.convertIfElseToSwitch",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.convertIfElseToSwitch",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.convertSwitchToIfElse",
           "when": "editorLangId == javascript"
@@ -448,6 +474,10 @@
           "command": "abracadabra.convertSwitchToIfElse",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.convertSwitchToIfElse",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.convertTernaryToIfElse",
           "when": "editorLangId == javascript"
@@ -464,6 +494,10 @@
           "command": "abracadabra.convertTernaryToIfElse",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.convertTernaryToIfElse",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.convertToTemplateLiteral",
           "when": "editorLangId == javascript"
@@ -480,6 +514,10 @@
           "command": "abracadabra.convertToTemplateLiteral",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.convertToTemplateLiteral",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.convertLetToConst",
           "when": "editorLangId == javascript"
@@ -496,6 +534,10 @@
           "command": "abracadabra.convertLetToConst",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.convertLetToConst",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.extract",
           "when": "editorLangId == javascript"
@@ -512,6 +554,10 @@
           "command": "abracadabra.extract",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.extract",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.extractInterface",
           "when": "editorLangId == typescript"
@@ -520,6 +566,10 @@
           "command": "abracadabra.extractInterface",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.extractInterface",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.flipIfElse",
           "when": "editorLangId == javascript"
@@ -536,6 +586,10 @@
           "command": "abracadabra.flipIfElse",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.flipIfElse",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.flipTernary",
           "when": "editorLangId == javascript"
@@ -552,6 +606,10 @@
           "command": "abracadabra.flipTernary",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.flipTernary",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.inline",
           "when": "editorLangId == javascript"
@@ -568,6 +626,10 @@
           "command": "abracadabra.inline",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.inline",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.liftUpConditional",
           "when": "editorLangId == javascript"
@@ -584,6 +646,10 @@
           "command": "abracadabra.liftUpConditional",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.liftUpConditional",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.mergeIfStatements",
           "when": "editorLangId == javascript"
@@ -600,6 +666,10 @@
           "command": "abracadabra.mergeIfStatements",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.mergeIfStatements",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.mergeWithPreviousIfStatement",
           "when": "editorLangId == javascript"
@@ -616,6 +686,10 @@
           "command": "abracadabra.mergeWithPreviousIfStatement",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.mergeWithPreviousIfStatement",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.moveStatementDown",
           "when": "editorLangId == javascript"
@@ -632,6 +706,10 @@
           "command": "abracadabra.moveStatementDown",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.moveStatementDown",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.moveStatementUp",
           "when": "editorLangId == javascript"
@@ -648,6 +726,10 @@
           "command": "abracadabra.moveStatementUp",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.moveStatementUp",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.negateExpression",
           "when": "editorLangId == javascript"
@@ -664,6 +746,10 @@
           "command": "abracadabra.negateExpression",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.negateExpression",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.react.addBracesToJsxAttribute",
           "when": "editorLangId == javascriptreact"
@@ -672,6 +758,10 @@
           "command": "abracadabra.react.addBracesToJsxAttribute",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.react.addBracesToJsxAttribute",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.react.convertToPureComponent",
           "when": "editorLangId == javascriptreact"
@@ -680,6 +770,10 @@
           "command": "abracadabra.react.convertToPureComponent",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.react.convertToPureComponent",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.react.removeBracesFromJsxAttribute",
           "when": "editorLangId == javascriptreact"
@@ -688,6 +782,10 @@
           "command": "abracadabra.react.removeBracesFromJsxAttribute",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.react.removeBracesFromJsxAttribute",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.removeBracesFromArrowFunction",
           "when": "editorLangId == javascript"
@@ -704,6 +802,10 @@
           "command": "abracadabra.removeBracesFromArrowFunction",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.removeBracesFromArrowFunction",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.removeBracesFromIfStatement",
           "when": "editorLangId == javascript"
@@ -720,6 +822,10 @@
           "command": "abracadabra.removeBracesFromIfStatement",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.removeBracesFromIfStatement",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.removeDeadCode",
           "when": "editorLangId == javascript"
@@ -736,6 +842,10 @@
           "command": "abracadabra.removeDeadCode",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.removeDeadCode",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.removeRedundantElse",
           "when": "editorLangId == javascript"
@@ -752,6 +862,10 @@
           "command": "abracadabra.removeRedundantElse",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.removeRedundantElse",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.renameSymbol",
           "when": "editorLangId == javascript"
@@ -768,6 +882,10 @@
           "command": "abracadabra.renameSymbol",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.renameSymbol",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.replaceBinaryWithAssignment",
           "when": "editorLangId == javascript"
@@ -784,6 +902,10 @@
           "command": "abracadabra.replaceBinaryWithAssignment",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.replaceBinaryWithAssignment",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.simplifyTernary",
           "when": "editorLangId == javascript"
@@ -800,6 +922,10 @@
           "command": "abracadabra.simplifyTernary",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.simplifyTernary",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.splitDeclarationAndInitialization",
           "when": "editorLangId == javascript"
@@ -816,6 +942,10 @@
           "command": "abracadabra.splitDeclarationAndInitialization",
           "when": "editorLangId == typescriptreact"
         },
+        {
+          "command": "abracadabra.splitDeclarationAndInitialization",
+          "when": "editorLangId == vue"
+        },
         {
           "command": "abracadabra.splitIfStatement",
           "when": "editorLangId == javascript"
@@ -831,6 +961,10 @@
         {
           "command": "abracadabra.splitIfStatement",
           "when": "editorLangId == typescriptreact"
+        },
+        {
+          "command": "abracadabra.splitIfStatement",
+          "when": "editorLangId == vue"
         }
       ]
     }
diff --git a/src/action-providers.ts b/src/action-providers.ts
index 9935a4c3..9899a25b 100644
--- a/src/action-providers.ts
+++ b/src/action-providers.ts
@@ -1,24 +1,19 @@
 import * as vscode from "vscode";
 
-import { createSelectionFromVSCode } from "./editor/adapters/vscode-editor";
 import { RefactoringWithActionProvider } from "./types";
 import * as t from "./ast";
 import { Selection } from "./editor/selection";
+import { createVSCodeEditor } from "./editor/adapters/create-vscode-editor";
 
 export { RefactoringActionProvider };
 
 type Refactoring = RefactoringWithActionProvider;
 
 class RefactoringActionProvider implements vscode.CodeActionProvider {
-  private refactorings: Refactoring[];
-
-  constructor(refactorings: Refactoring[]) {
-    this.refactorings = refactorings;
-  }
+  constructor(private refactorings: Refactoring[]) {}
 
   provideCodeActions(
-    document: vscode.TextDocument,
-    range: vscode.Range | vscode.Selection
+    document: vscode.TextDocument
   ): vscode.ProviderResult<vscode.CodeAction[]> {
     const NO_ACTION: vscode.CodeAction[] = [];
 
@@ -26,13 +21,15 @@ class RefactoringActionProvider implements vscode.CodeActionProvider {
       return NO_ACTION;
     }
 
+    const editor = createVSCodeEditor();
+    if (!editor) return NO_ACTION;
+
     try {
-      const ast = t.parse(document.getText());
-      const selection = createSelectionFromVSCode(range);
+      const ast = t.parse(editor.code);
 
       return this.findApplicableRefactorings(
         ast,
-        selection
+        editor.selection
       ).map((refactoring) => this.buildCodeActionFor(refactoring));
     } catch {
       // Silently fail, we don't care why it failed (e.g. code can't be parsed).
diff --git a/src/commands.ts b/src/commands.ts
index 413e6cdf..fa987c01 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -1,16 +1,16 @@
 import * as vscode from "vscode";
 
 import { Operation } from "./types";
-import { VSCodeEditor } from "./editor/adapters/vscode-editor";
+import { createVSCodeEditor } from "./editor/adapters/create-vscode-editor";
 
 export { createCommand, executeSafely };
 
 function createCommand(execute: Operation) {
   return async () => {
-    const activeTextEditor = vscode.window.activeTextEditor;
-    if (!activeTextEditor) return;
+    const editor = createVSCodeEditor();
+    if (!editor) return;
 
-    await executeSafely(() => execute(new VSCodeEditor(activeTextEditor)));
+    await executeSafely(() => execute(editor));
   };
 }
 
diff --git a/src/editor/adapters/create-vscode-editor.ts b/src/editor/adapters/create-vscode-editor.ts
new file mode 100644
index 00000000..661b3f61
--- /dev/null
+++ b/src/editor/adapters/create-vscode-editor.ts
@@ -0,0 +1,15 @@
+import * as vscode from "vscode";
+
+import { VSCodeEditor } from "./vscode-editor";
+import { VueVSCodeEditor } from "./vue-vscode-editor";
+
+export { createVSCodeEditor };
+
+function createVSCodeEditor(): VSCodeEditor | undefined {
+  const activeTextEditor = vscode.window.activeTextEditor;
+  if (!activeTextEditor) return;
+
+  return activeTextEditor.document.languageId === "vue"
+    ? new VueVSCodeEditor(activeTextEditor)
+    : new VSCodeEditor(activeTextEditor);
+}
diff --git a/src/editor/adapters/vscode-editor.ts b/src/editor/adapters/vscode-editor.ts
index 2508a423..2f4d309a 100644
--- a/src/editor/adapters/vscode-editor.ts
+++ b/src/editor/adapters/vscode-editor.ts
@@ -12,7 +12,7 @@ import {
 import { Selection } from "../selection";
 import { Position } from "../position";
 
-export { VSCodeEditor, createSelectionFromVSCode };
+export { VSCodeEditor };
 
 class VSCodeEditor implements Editor {
   private editor: vscode.TextEditor;
@@ -39,13 +39,7 @@ class VSCodeEditor implements Editor {
     );
 
     const edit = new vscode.WorkspaceEdit();
-    const allDocumentRange = new vscode.Range(
-      new vscode.Position(0, 0),
-      new vscode.Position(this.document.lineCount, 0)
-    );
-
-    edit.set(this.document.uri, [new vscode.TextEdit(allDocumentRange, code)]);
-
+    edit.set(this.document.uri, [new vscode.TextEdit(this.editRange, code)]);
     await vscode.workspace.applyEdit(edit);
 
     // Put cursor at correct position
@@ -63,6 +57,13 @@ class VSCodeEditor implements Editor {
     }
   }
 
+  protected get editRange(): vscode.Range {
+    return new vscode.Range(
+      new vscode.Position(0, 0),
+      new vscode.Position(this.document.lineCount, 0)
+    );
+  }
+
   async readThenWrite(
     selection: Selection,
     getModifications: (code: Code) => Modification[],
diff --git a/src/editor/adapters/vue-vscode-editor.ts b/src/editor/adapters/vue-vscode-editor.ts
new file mode 100644
index 00000000..b5823915
--- /dev/null
+++ b/src/editor/adapters/vue-vscode-editor.ts
@@ -0,0 +1,98 @@
+import * as vscode from "vscode";
+
+import { Code, Modification } from "../editor";
+import { Position } from "../position";
+import { Selection } from "../selection";
+import { VSCodeEditor } from "./vscode-editor";
+
+export { VueVSCodeEditor };
+
+class VueVSCodeEditor extends VSCodeEditor {
+  get code(): Code {
+    return super.code.slice(this.openingTagOffset, this.closingTagOffset);
+  }
+
+  get selection(): Selection {
+    return this.offsetEditorSelection(super.selection);
+  }
+
+  async write(code: Code, newCursorPosition?: Position): Promise<void> {
+    return super.write(
+      code,
+      newCursorPosition && this.offsetPosition(newCursorPosition)
+    );
+  }
+
+  protected get editRange(): vscode.Range {
+    return new vscode.Range(
+      new vscode.Position(
+        this.toOffsetLinesCount(this.openingTagOffset),
+        this.openingTag.length
+      ),
+      new vscode.Position(this.toOffsetLinesCount(this.closingTagOffset), 0)
+    );
+  }
+
+  async readThenWrite(
+    selection: Selection,
+    getModifications: (code: Code) => Modification[],
+    newCursorPosition?: Position
+  ): Promise<void> {
+    const getOffsetModifications = (code: Code) => {
+      return getModifications(code).map(({ code, selection }) => ({
+        code,
+        selection: this.offsetSelection(selection)
+      }));
+    };
+
+    return super.readThenWrite(
+      this.offsetSelection(selection),
+      getOffsetModifications,
+      newCursorPosition && this.offsetPosition(newCursorPosition)
+    );
+  }
+
+  moveCursorTo(position: Position) {
+    return super.moveCursorTo(this.offsetPosition(position));
+  }
+
+  private offsetEditorSelection(selection: Selection): Selection {
+    const offsetLinesCount = this.toOffsetLinesCount(this.openingTagOffset);
+
+    return Selection.fromPositions(
+      selection.start.removeLines(offsetLinesCount),
+      selection.end.removeLines(offsetLinesCount)
+    );
+  }
+
+  private offsetSelection(selection: Selection): Selection {
+    const offsetLinesCount = this.toOffsetLinesCount(this.openingTagOffset);
+
+    return Selection.fromPositions(
+      selection.start.addLines(offsetLinesCount),
+      selection.end.addLines(offsetLinesCount)
+    );
+  }
+
+  private offsetPosition(position: Position): Position {
+    return position.addLines(this.toOffsetLinesCount(this.openingTagOffset));
+  }
+
+  private toOffsetLinesCount(offset: Offset): number {
+    return super.code.slice(0, offset).split("\n").length - 1;
+  }
+
+  private get openingTagOffset(): Offset {
+    return super.code.indexOf(this.openingTag) + this.openingTag.length;
+  }
+
+  private get openingTag(): string {
+    return "<script>";
+  }
+
+  private get closingTagOffset(): Offset {
+    return super.code.indexOf("</script>");
+  }
+}
+
+type Offset = number;
diff --git a/src/editor/attempting-editor.ts b/src/editor/attempting-editor.ts
new file mode 100644
index 00000000..ec280cb7
--- /dev/null
+++ b/src/editor/attempting-editor.ts
@@ -0,0 +1,63 @@
+import {
+  Choice,
+  Code,
+  Command,
+  Editor,
+  ErrorReason,
+  Modification
+} from "./editor";
+import { Position } from "./position";
+import { Selection } from "./selection";
+
+export { AttemptingEditor };
+
+class AttemptingEditor implements Editor {
+  attemptSucceeded = true;
+
+  constructor(private editor: Editor, private expectedReason: ErrorReason) {}
+
+  get code(): Code {
+    return this.editor.code;
+  }
+
+  get selection(): Selection {
+    return this.editor.selection;
+  }
+
+  write(code: Code, newCursorPosition?: Position): Promise<void> {
+    return this.editor.write(code, newCursorPosition);
+  }
+
+  readThenWrite(
+    selection: Selection,
+    getModifications: (code: Code) => Modification[],
+    newCursorPosition?: Position
+  ): Promise<void> {
+    return this.editor.readThenWrite(
+      selection,
+      getModifications,
+      newCursorPosition
+    );
+  }
+
+  delegate(command: Command): Promise<void> {
+    return this.editor.delegate(command);
+  }
+
+  async showError(reason: ErrorReason) {
+    if (reason === this.expectedReason) {
+      this.attemptSucceeded = false;
+      return Promise.resolve();
+    }
+
+    await this.editor.showError(reason);
+  }
+
+  askUser<T>(choices: Choice<T>[]): Promise<Choice<T> | undefined> {
+    return this.editor.askUser(choices);
+  }
+
+  moveCursorTo(position: Position): Promise<void> {
+    return this.editor.moveCursorTo(position);
+  }
+}
diff --git a/src/editor/position.ts b/src/editor/position.ts
index fff644e7..86b29cea 100644
--- a/src/editor/position.ts
+++ b/src/editor/position.ts
@@ -73,6 +73,10 @@ class Position {
     return new Position(this.line + lines, this.character);
   }
 
+  removeLines(lines: number): Position {
+    return new Position(this.line - lines, this.character);
+  }
+
   addCharacters(characters: number): Position {
     return new Position(this.line, this.character + characters);
   }
diff --git a/src/extension.ts b/src/extension.ts
index e6076416..2a8a7aa0 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -43,7 +43,8 @@ const ALL_LANGUAGES = [
   "javascript",
   "javascriptreact",
   "typescript",
-  "typescriptreact"
+  "typescriptreact",
+  "vue"
 ];
 
 export function activate(context: vscode.ExtensionContext) {
diff --git a/src/refactorings/extract/index.ts b/src/refactorings/extract/index.ts
index c22df8d0..ce94c212 100644
--- a/src/refactorings/extract/index.ts
+++ b/src/refactorings/extract/index.ts
@@ -1,13 +1,11 @@
-import * as vscode from "vscode";
-
 import { extractGenericType } from "./extract-generic-type/extract-generic-type";
 import { extractVariable } from "./extract-variable/extract-variable";
 
+import { Refactoring } from "../../types";
 import { executeSafely } from "../../commands";
 import { ErrorReason } from "../../editor/editor";
-import { VSCodeEditor } from "../../editor/adapters/vscode-editor";
-
-import { Refactoring } from "../../types";
+import { AttemptingEditor } from "../../editor/attempting-editor";
+import { createVSCodeEditor } from "../../editor/adapters/create-vscode-editor";
 
 const config: Refactoring = {
   command: {
@@ -19,29 +17,19 @@ const config: Refactoring = {
 export default config;
 
 async function extract() {
-  const activeTextEditor = vscode.window.activeTextEditor;
-  if (!activeTextEditor) return;
+  const vscodeEditor = createVSCodeEditor();
+  if (!vscodeEditor) return;
+
+  const attemptingEditor = new AttemptingEditor(
+    vscodeEditor,
+    ErrorReason.DidNotFindExtractableCode
+  );
 
   await executeSafely(async () => {
-    await extractVariable(
-      new VSCodeEditorAttemptingExtraction(activeTextEditor)
-    );
+    await extractVariable(attemptingEditor);
 
-    if (!new VSCodeEditorAttemptingExtraction(activeTextEditor).couldExtract) {
-      await extractGenericType(new VSCodeEditor(activeTextEditor));
+    if (!attemptingEditor.attemptSucceeded) {
+      await extractGenericType(vscodeEditor);
     }
   });
 }
-
-class VSCodeEditorAttemptingExtraction extends VSCodeEditor {
-  couldExtract = true;
-
-  async showError(reason: ErrorReason) {
-    if (reason === ErrorReason.DidNotFindExtractableCode) {
-      this.couldExtract = false;
-      return Promise.resolve();
-    }
-
-    await super.showError(reason);
-  }
-}
diff --git a/src/refactorings/inline/index.ts b/src/refactorings/inline/index.ts
index 90b76a5d..b6fae66c 100644
--- a/src/refactorings/inline/index.ts
+++ b/src/refactorings/inline/index.ts
@@ -1,13 +1,11 @@
-import * as vscode from "vscode";
-
 import { inlineFunction } from "./inline-function/inline-function";
 import { inlineVariable } from "./inline-variable/inline-variable";
 
 import { executeSafely } from "../../commands";
 import { ErrorReason } from "../../editor/editor";
-import { VSCodeEditor } from "../../editor/adapters/vscode-editor";
-
+import { AttemptingEditor } from "../../editor/attempting-editor";
 import { Refactoring } from "../../types";
+import { createVSCodeEditor } from "../../editor/adapters/create-vscode-editor";
 
 const config: Refactoring = {
   command: {
@@ -19,29 +17,19 @@ const config: Refactoring = {
 export default config;
 
 async function inline() {
-  const activeTextEditor = vscode.window.activeTextEditor;
-  if (!activeTextEditor) {
-    return;
-  }
+  const vscodeEditor = createVSCodeEditor();
+  if (!vscodeEditor) return;
+
+  const attemptingEditor = new AttemptingEditor(
+    vscodeEditor,
+    ErrorReason.DidNotFindInlinableCode
+  );
 
   await executeSafely(async () => {
-    await inlineVariable(new VSCodeEditorAttemptingInlining(activeTextEditor));
+    await inlineVariable(attemptingEditor);
 
-    if (!new VSCodeEditorAttemptingInlining(activeTextEditor).couldInlineCode) {
-      await inlineFunction(new VSCodeEditor(activeTextEditor));
+    if (!attemptingEditor.attemptSucceeded) {
+      await inlineFunction(vscodeEditor);
     }
   });
 }
-
-class VSCodeEditorAttemptingInlining extends VSCodeEditor {
-  couldInlineCode = true;
-
-  async showError(reason: ErrorReason) {
-    if (reason === ErrorReason.DidNotFindInlinableCode) {
-      this.couldInlineCode = false;
-      return Promise.resolve();
-    }
-
-    await super.showError(reason);
-  }
-}
diff --git a/src/refactorings/move-statement-up/move-statement-up.ts b/src/refactorings/move-statement-up/move-statement-up.ts
index 5d6ad3a3..397f8eab 100644
--- a/src/refactorings/move-statement-up/move-statement-up.ts
+++ b/src/refactorings/move-statement-up/move-statement-up.ts
@@ -92,7 +92,7 @@ function updateCode(
     if (hasComments(pathAbove)) {
       pathAbove.node.leadingComments.forEach((comment) => {
         const { height } = Selection.fromAST(comment.loc);
-        newStatementPosition = newStatementPosition.addLines(-(height + 1));
+        newStatementPosition = newStatementPosition.removeLines(height + 1);
       });
     }
 


Test patch:
diff --git a/src/editor/attempting-editor.test.ts b/src/editor/attempting-editor.test.ts
new file mode 100644
index 00000000..7b714259
--- /dev/null
+++ b/src/editor/attempting-editor.test.ts
@@ -0,0 +1,52 @@
+import { InMemoryEditor } from "./adapters/in-memory-editor";
+import { ErrorReason } from "./error-reason";
+import { AttemptingEditor } from "./attempting-editor";
+
+describe("AttemptingEditor", () => {
+  const EXPECTED_REASON = ErrorReason.DidNotFindInlinableCode;
+  const ANY_OTHER_REASON = ErrorReason.DidNotFindLetToConvertToConst;
+
+  describe("reason is expected", () => {
+    it("should not ask editor to show error", async () => {
+      const editor = new InMemoryEditor("// irrelevant");
+      spyOn(editor, "showError");
+      const attemptingEditor = new AttemptingEditor(editor, EXPECTED_REASON);
+
+      attemptingEditor.showError(EXPECTED_REASON);
+
+      expect(editor.showError).not.toBeCalled();
+    });
+
+    it("should say attempt didn't succeeded", async () => {
+      const editor = new InMemoryEditor("// irrelevant");
+      const attemptingEditor = new AttemptingEditor(editor, EXPECTED_REASON);
+      expect(attemptingEditor.attemptSucceeded).toBe(true);
+
+      attemptingEditor.showError(EXPECTED_REASON);
+
+      expect(attemptingEditor.attemptSucceeded).toBe(false);
+    });
+  });
+
+  describe("reason is not expected", () => {
+    it("should ask editor to show error", async () => {
+      const editor = new InMemoryEditor("// irrelevant");
+      spyOn(editor, "showError");
+      const attemptingEditor = new AttemptingEditor(editor, EXPECTED_REASON);
+
+      attemptingEditor.showError(ANY_OTHER_REASON);
+
+      expect(editor.showError).toBeCalledTimes(1);
+      expect(editor.showError).toBeCalledWith(ANY_OTHER_REASON);
+    });
+
+    it("should say attempt succeeded", async () => {
+      const editor = new InMemoryEditor("// irrelevant");
+      const attemptingEditor = new AttemptingEditor(editor, EXPECTED_REASON);
+
+      attemptingEditor.showError(ANY_OTHER_REASON);
+
+      expect(attemptingEditor.attemptSucceeded).toBe(true);
+    });
+  });
+});