diff --git a/data/playground/elixir/calls.ex b/data/playground/elixir/calls.ex new file mode 100644 index 0000000000..36506dbbb7 --- /dev/null +++ b/data/playground/elixir/calls.ex @@ -0,0 +1,19 @@ +defmodule Calls do + a() + b.() + c do + 1 + end + d() do + end + e() do + 1 + end + a x do + x + end + f(0) do + 1 + end + Alias.g() +end diff --git a/data/playground/elixir/data-structures.ex b/data/playground/elixir/data-structures.ex new file mode 100644 index 0000000000..d13657effe --- /dev/null +++ b/data/playground/elixir/data-structures.ex @@ -0,0 +1,36 @@ +defmodule DataStructures do + [] + [a] + [A] + [1] + [1, 2] + [[1], 1] + %{} + %{a: 1, b: 2} + %{:a => 1, "b" => 2, c => 3} + %{"a" => 1, b: 2, c: 3} + %{user | name: "Jane", email: "jane@example.com"} + %{user | "name" => "Jane"} + %AStruct{a: 1, b: 2} + %AnotherStruct{:a => 1, "b" => 2, c => 3} + %ThirdStruct{"a" => 1, b: 2, c: 3} + %User{user | name: "Jane", email: "jane@example.com"} + %User{user | "name" => "Jane"} + %{ + :a => 1, + "b" => 2, + c => 3 + } + %_{} + %name{} + %^name{} + %__MODULE__{} + %__MODULE__.Child{} + %:"Elixir.Mod"{} + %fun(){} + %Mod.fun(){} + %fun.(){} + {} + {1} + {1, 2} +end diff --git a/data/playground/elixir/functions.ex b/data/playground/elixir/functions.ex new file mode 100644 index 0000000000..1724f87973 --- /dev/null +++ b/data/playground/elixir/functions.ex @@ -0,0 +1,62 @@ +defmodule Funcs do + def no_args() do + end + + def no_args_no_parens do + end + + def one_arg(x) do + x + end + + def one_arg_no_parens(x) do + x + end + + def two_args(x, y) do + x + y + end + + def two_args_no_parens(x, y) do + x + y + end + + def default_args_no_parens(x, y \\ 1) do + x + y + end + + def default_args(x, y \\ 1) do + x + y + end + + def do_block(), do: 1 + def do_block(x), do: x + + def pattern_matching([{x, y} | tail]) do + x + y + end + + def one_guard(x) when x == 1 do + x + end + + def multiple_guard(x) when x > 10 when x < 5 do + x + end + + defp private(x) do + x + end + + defmacro macro(x) do + quote do + [unquote(x)] + end + end + + defguard guard(term) when is_integer(term) and rem(term, 2) == 0 + + def unquote(name)(unquote_splicing(args)) do + unquote(compiled) + end +end diff --git a/packages/common/src/extensionDependencies.ts b/packages/common/src/extensionDependencies.ts new file mode 100644 index 0000000000..3043dfa9e9 --- /dev/null +++ b/packages/common/src/extensionDependencies.ts @@ -0,0 +1,13 @@ +export const extensionDependencies = [ + // Cursorless access to Tree sitter + "pokey.parse-tree", + + // Register necessary language-IDs for tests + "jakebecker.elixir-ls", // elixir + "jrieken.vscode-tree-sitter-query", // scm + "mrob95.vscode-talonscript", // talon + "scala-lang.scala", // scala + + // Necessary for the `drink cell` and `pour cell` tests + "ms-toolsai.jupyter", +]; diff --git a/packages/common/src/scopeSupportFacets/elixir.ts b/packages/common/src/scopeSupportFacets/elixir.ts new file mode 100644 index 0000000000..82e72ebcb5 --- /dev/null +++ b/packages/common/src/scopeSupportFacets/elixir.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { + LanguageScopeSupportFacetMap, + ScopeSupportFacetLevel, +} from "./scopeSupportFacets.types"; + +const { supported } = ScopeSupportFacetLevel; + +export const elixirScopeSupport: LanguageScopeSupportFacetMap = { + "collectionItem.map": supported, + "collectionItem.map.iteration": supported, +}; diff --git a/packages/common/src/scopeSupportFacets/getLanguageScopeSupport.ts b/packages/common/src/scopeSupportFacets/getLanguageScopeSupport.ts new file mode 100644 index 0000000000..6b525c0d0a --- /dev/null +++ b/packages/common/src/scopeSupportFacets/getLanguageScopeSupport.ts @@ -0,0 +1,33 @@ +import { elixirScopeSupport } from "./elixir"; +import { htmlScopeSupport } from "./html"; +import { javaScopeSupport } from "./java"; +import { javascriptScopeSupport } from "./javascript"; +import { jsonScopeSupport } from "./json"; +import { pythonScopeSupport } from "./python"; +import { LanguageScopeSupportFacetMap } from "./scopeSupportFacets.types"; +import { talonScopeSupport } from "./talon"; +import { typescriptScopeSupport } from "./typescript"; + +export function getLanguageScopeSupport( + languageId: string, +): LanguageScopeSupportFacetMap { + switch (languageId) { + case "html": + return htmlScopeSupport; + case "java": + return javaScopeSupport; + case "javascript": + return javascriptScopeSupport; + case "json": + return jsonScopeSupport; + case "python": + return pythonScopeSupport; + case "talon": + return talonScopeSupport; + case "typescript": + return typescriptScopeSupport; + case "elixir": + return elixirScopeSupport; + } + throw Error(`Unsupported language: '${languageId}'`); +} diff --git a/packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts b/packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts new file mode 100644 index 0000000000..5005116840 --- /dev/null +++ b/packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts @@ -0,0 +1,331 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { + ScopeSupportFacet, + ScopeSupportFacetInfo, +} from "./scopeSupportFacets.types"; + +export const scopeSupportFacetInfos: Record< + ScopeSupportFacet, + ScopeSupportFacetInfo +> = { + command: { + description: "A command, for example Talon spoken command or bash", + scopeType: "command", + }, + + element: { + description: "An xml/html element. Also used for LaTeX environments", + scopeType: "xmlElement", + }, + startTag: { + description: "The start tag of an xml element", + scopeType: "xmlStartTag", + }, + endTag: { + description: "The end tag of an xml element", + scopeType: "xmlEndTag", + }, + tags: { + description: "Both tags in an xml element", + scopeType: "xmlBothTags", + }, + attribute: { + description: "A attribute, eg of an html element or a C++ attribute", + scopeType: "attribute", + }, + + list: { + description: "A list/array", + scopeType: "list", + }, + map: { + description: "A map/dictionary", + scopeType: "map", + }, + ifStatement: { + description: "An if statement", + scopeType: "ifStatement", + }, + regularExpression: { + description: "A regular expression", + scopeType: "regularExpression", + }, + switchStatementSubject: { + description: "The subject of a switch statement", + scopeType: "private.switchStatementSubject", + }, + fieldAccess: { + description: "A field access", + scopeType: "private.fieldAccess", + }, + + statement: { + description: "A statement, eg assignment, for loop, etc", + scopeType: "statement", + }, + "statement.iteration.document": { + description: "Iteration scope for statements. The entire document.", + scopeType: "statement", + isIteration: true, + }, + "statement.iteration.block": { + description: + "Iteration scope for statements. Statement blocks(body of functions/if statements/for loops/etc).", + scopeType: "statement", + isIteration: true, + }, + + class: { + description: "A class in an object-oriented language", + scopeType: "class", + }, + className: { + description: "The name of a class", + scopeType: "className", + }, + + namedFunction: { + description: "A named function declaration", + scopeType: "namedFunction", + }, + "namedFunction.method": { + description: "A named method declaration in a class", + scopeType: "namedFunction", + }, + anonymousFunction: { + description: + "An anonymous function, eg a lambda function, an arrow function, etc", + scopeType: "anonymousFunction", + }, + functionName: { + description: "The name of a function", + scopeType: "functionName", + }, + + functionCall: { + description: "A function call", + scopeType: "functionCall", + }, + "functionCall.constructor": { + description: "A constructor call", + scopeType: "functionCall", + }, + functionCallee: { + description: "The function being called in a function call", + scopeType: "functionCallee", + }, + "functionCallee.constructor": { + description: + "The class being constructed in a class instantiation, including the `new` keyword", + scopeType: "functionCallee", + }, + + "argument.actual": { + description: "An argument/parameter in a function call", + scopeType: "argumentOrParameter", + }, + "argument.actual.iteration": { + description: + "Iteration scope of arguments in a function call, should be inside the parens of the argument list", + scopeType: "argumentOrParameter", + isIteration: true, + }, + "argument.formal": { + description: "A parameter in a function declaration", + scopeType: "argumentOrParameter", + }, + "argument.formal.iteration": { + description: + "Iteration scope of the formal parameters of a function declaration; should be the whole parameter list", + scopeType: "argumentOrParameter", + isIteration: true, + }, + "collectionItem.map": { + description: "An entry in a map/dictionary", + scopeType: "collectionItem", + }, + "collectionItem.map.iteration": { + description: + "Iteration scope for entries in a map/dictionary; should be between the braces", + scopeType: "collectionItem", + isIteration: true, + }, + + "comment.line": { + description: "A line comment", + scopeType: "comment", + }, + "comment.block": { + description: "A block comment", + scopeType: "comment", + }, + + "string.singleLine": { + description: "A single-line string", + scopeType: "string", + }, + "string.multiLine": { + description: "A multi-line string", + scopeType: "string", + }, + + "branch.if": { + description: "An if/elif/else branch", + scopeType: "branch", + }, + + "branch.if.iteration": { + description: + "Iteration scope for if/elif/else branch; should be the entire if-else statement", + scopeType: "branch", + isIteration: true, + }, + "branch.try": { + description: "A try/catch/finally branch", + scopeType: "branch", + }, + "branch.switchCase": { + description: "A case/default branch in a switch/match statement", + scopeType: "branch", + }, + "branch.switchCase.iteration": { + description: + "Iteration scope for branches in a switch/match statement; should contain all the cases", + scopeType: "branch", + isIteration: true, + }, + "branch.ternary": { + description: "A branch in a ternary expression", + scopeType: "branch", + }, + + "condition.if": { + description: "A condition in an if statement", + scopeType: "condition", + }, + "condition.while": { + description: "A condition in a while loop", + scopeType: "condition", + }, + "condition.doWhile": { + description: "A condition in a do while loop", + scopeType: "condition", + }, + "condition.for": { + description: "A condition in a for loop", + scopeType: "condition", + }, + "condition.ternary": { + description: "A condition in a ternary expression", + scopeType: "condition", + }, + "condition.switchCase": { + description: "A condition in a switch statement", + scopeType: "condition", + }, + + "name.assignment": { + description: "Name (LHS) of an assignment", + scopeType: "name", + }, + "name.assignment.pattern": { + description: "LHS of an assignment with pattern destructuring", + scopeType: "name", + }, + "name.foreach": { + description: "Iteration variable name in a for each loop", + scopeType: "name", + }, + "name.function": { + description: "Name of a function", + scopeType: "name", + }, + "name.class": { + description: "Name of a class", + scopeType: "name", + }, + "name.field": { + description: "Name (LHS) of a field in a class / interface", + scopeType: "name", + }, + + "key.attribute": { + description: "Key (LHS) of an attribute eg in an xml element", + scopeType: "collectionKey", + }, + "key.mapPair": { + description: "Key (LHS) of a key-value pair of a map", + scopeType: "collectionKey", + }, + "key.mapPair.iteration": { + description: + "Iteration scope of key-value pairs in a map; should be between the braces", + scopeType: "collectionKey", + isIteration: true, + }, + + "value.assignment": { + description: "Value (RHS) of an assignment", + scopeType: "value", + }, + "value.mapPair": { + description: "Value (RHS) of a key-value pair in a map", + scopeType: "value", + }, + "value.mapPair.iteration": { + description: + "Iteration scope of key-value pairs in a map; should be between the braces", + scopeType: "value", + isIteration: true, + }, + "value.foreach": { + description: "Iterable in a for each loop", + scopeType: "value", + }, + "value.attribute": { + description: "Value (RHS) of an attribute eg in an xml element", + scopeType: "value", + }, + "value.return": { + description: "Return value of a function", + scopeType: "value", + }, + "value.return.lambda": { + description: "Implicit return value from a lambda", + scopeType: "value", + }, + "value.field": { + description: "Value (RHS) of a field in a class / interface", + scopeType: "value", + }, + + "type.assignment": { + description: "Type of variable in an assignment", + scopeType: "type", + }, + "type.formalParameter": { + description: "Type of formal parameter in a function declaration", + scopeType: "type", + }, + "type.return": { + description: "Type of return value in a function declaration", + scopeType: "type", + }, + "type.field": { + description: "Type of field in a class / interface", + scopeType: "type", + }, + "type.foreach": { + description: "Type of variable in a for each loop", + scopeType: "type", + }, + "type.interface": { + description: "An interface declaration", + scopeType: "type", + }, + "type.alias": { + description: "A type alias declaration", + scopeType: "type", + }, +}; diff --git a/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts new file mode 100644 index 0000000000..1e2473b7a7 --- /dev/null +++ b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts @@ -0,0 +1,134 @@ +import { SimpleScopeTypeType } from "../types/command/PartialTargetDescriptor.types"; + +const scopeSupportFacets = [ + "command", + + "element", + "tags", + "startTag", + "endTag", + "attribute", + + "list", + "map", + "ifStatement", + "regularExpression", + "switchStatementSubject", + "fieldAccess", + + "statement", + "statement.iteration.document", + "statement.iteration.block", + + "class", + "className", + "namedFunction", + "namedFunction.method", + "anonymousFunction", + "functionName", + + "functionCall", + "functionCall.constructor", + "functionCallee", + "functionCallee.constructor", + + "argument.actual", + "argument.actual.iteration", + "argument.formal", + "argument.formal.iteration", + + "collectionItem.map", + "collectionItem.map.iteration", + + "comment.line", + "comment.block", + + "string.singleLine", + "string.multiLine", + + "branch.if", + "branch.if.iteration", + "branch.try", + "branch.switchCase", + "branch.switchCase.iteration", + "branch.ternary", + + "condition.if", + "condition.while", + "condition.doWhile", + "condition.for", + "condition.ternary", + "condition.switchCase", + + "name.assignment", + "name.assignment.pattern", + "name.foreach", + "name.function", + "name.class", + "name.field", + + "key.attribute", + "key.mapPair", + "key.mapPair.iteration", + + "value.assignment", + "value.mapPair", + "value.mapPair.iteration", + "value.attribute", + "value.foreach", + "value.return", + "value.return.lambda", + "value.field", + + "type.assignment", + "type.formalParameter", + "type.return", + "type.field", + "type.foreach", + "type.interface", + "type.alias", + + // FIXME: Still in legacy + // section + // selector + // unit + // collectionItem + // textFragment +] as const; + +const textualScopeSupportFacets = [ + "character", + "word", + "token", + "identifier", + "line", + "sentence", + "paragraph", + "document", + "nonWhitespaceSequence", + // FIXME: Still in legacy + // "boundedNonWhitespaceSequence", + "url", +] as const; + +export interface ScopeSupportFacetInfo { + readonly description: string; + readonly scopeType: SimpleScopeTypeType; + readonly isIteration?: boolean; +} + +export enum ScopeSupportFacetLevel { + supported, + supportedLegacy, + unsupported, + notApplicable, +} + +export type ScopeSupportFacet = (typeof scopeSupportFacets)[number]; + +export type TextualScopeSupportFacet = + (typeof textualScopeSupportFacets)[number]; + +export type LanguageScopeSupportFacetMap = Partial< + Record +>; diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk.yml new file mode 100644 index 0000000000..591582707e --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk.yml @@ -0,0 +1,27 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: | + def fun() do + # body + end + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk10.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk10.yml new file mode 100644 index 0000000000..3cbf333698 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk10.yml @@ -0,0 +1,26 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + def fun(x) when x == 1 do + x + end + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk11.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk11.yml new file mode 100644 index 0000000000..c70aeebb75 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk11.yml @@ -0,0 +1,26 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + defp fun(x) do + x + end + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk12.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk12.yml new file mode 100644 index 0000000000..afe4bd0370 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk12.yml @@ -0,0 +1,28 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + defmacro fun(x) do + quote do + [unquote(x)] + end + end + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk13.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk13.yml new file mode 100644 index 0000000000..30767e532b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk13.yml @@ -0,0 +1,25 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: | + defguard is_even(term) when is_integer(term) and rem(term, 2) == 0 + selections: + - anchor: {line: 0, character: 26} + active: {line: 0, character: 26} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk14.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk14.yml new file mode 100644 index 0000000000..7de4c4a748 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk14.yml @@ -0,0 +1,22 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + a x do + x + end + selections: + - anchor: {line: 1, character: 3} + active: {line: 1, character: 3} + marks: {} +thrownError: {name: NoContainingScopeError} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk2.yml new file mode 100644 index 0000000000..96cc853d71 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk2.yml @@ -0,0 +1,27 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: | + def fun do + # body + end + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk3.yml new file mode 100644 index 0000000000..31bd410663 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk3.yml @@ -0,0 +1,25 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: | + def fun, do: 1 + selections: + - anchor: {line: 0, character: 14} + active: {line: 0, character: 14} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk4.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk4.yml new file mode 100644 index 0000000000..a2e32aba68 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk4.yml @@ -0,0 +1,25 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: | + def fun(), do: 1 + selections: + - anchor: {line: 0, character: 16} + active: {line: 0, character: 16} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk5.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk5.yml new file mode 100644 index 0000000000..c10c01c339 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk5.yml @@ -0,0 +1,26 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + def fun(x) do + x + end + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk6.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk6.yml new file mode 100644 index 0000000000..d9ec85f355 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk6.yml @@ -0,0 +1,26 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + def fun x do + x + end + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk7.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk7.yml new file mode 100644 index 0000000000..cb6bfc73bf --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk7.yml @@ -0,0 +1,27 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: | + def fun(x, y) do + x + y + end + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk8.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk8.yml new file mode 100644 index 0000000000..f842c7f924 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk8.yml @@ -0,0 +1,26 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + def fun x, y do + x + y + end + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk9.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk9.yml new file mode 100644 index 0000000000..774bbc8ce6 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunk9.yml @@ -0,0 +1,26 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + def fun(x, y \\ 1) do + x + y + end + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunkName.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunkName.yml new file mode 100644 index 0000000000..6b12bca374 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeFunkName.yml @@ -0,0 +1,29 @@ +languageId: elixir +command: + version: 6 + spokenForm: change funk name + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: functionName} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + def elixir(x, something) do + x = 5 + end + selections: + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} + marks: {} +finalState: + documentContents: |- + def (x, something) do + x = 5 + end + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeKey.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeKey.yml new file mode 100644 index 0000000000..f94c03f260 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeKey.yml @@ -0,0 +1,33 @@ +languageId: elixir +command: + version: 6 + spokenForm: change key + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionKey} + usePrePhraseSnapshot: true +initialState: + documentContents: | + %{ + :a => "lorem", + "b" => "ipsum", + 3 => "dolor" + } + selections: + - anchor: {line: 2, character: 13} + active: {line: 2, character: 13} + marks: {} +finalState: + documentContents: | + %{ + :a => "lorem", + => "ipsum", + 3 => "dolor" + } + selections: + - anchor: {line: 2, character: 2} + active: {line: 2, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeKeyFine.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeKeyFine.yml new file mode 100644 index 0000000000..f2bb375041 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeKeyFine.yml @@ -0,0 +1,27 @@ +languageId: elixir +command: + version: 6 + spokenForm: change key fine + action: + name: clearAndSetSelection + target: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + modifiers: + - type: containingScope + scopeType: {type: collectionKey} + usePrePhraseSnapshot: true +initialState: + documentContents: "%{ \"foo\" => \"lorem\", \"bar\" => \"ipsum\", \"baz\" => \"dolor\"}" + selections: + - anchor: {line: 0, character: 56} + active: {line: 0, character: 56} + marks: + default.f: + start: {line: 0, character: 4} + end: {line: 0, character: 7} +finalState: + documentContents: "%{ => \"lorem\", \"bar\" => \"ipsum\", \"baz\" => \"dolor\"}" + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeLambda.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeLambda.yml new file mode 100644 index 0000000000..2b9110f3b1 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeLambda.yml @@ -0,0 +1,23 @@ +languageId: elixir +command: + version: 6 + spokenForm: change lambda + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: anonymousFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: add = fn x, y -> x + y end + selections: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} + marks: {} +finalState: + documentContents: "add = " + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeNextComment.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeNextComment.yml new file mode 100644 index 0000000000..93d411054f --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeNextComment.yml @@ -0,0 +1,29 @@ +languageId: elixir +command: + version: 6 + spokenForm: change next comment + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: comment} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true +initialState: + documentContents: |- + + # blah + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeValue.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeValue.yml new file mode 100644 index 0000000000..21d197cbbb --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/changeValue.yml @@ -0,0 +1,33 @@ +languageId: elixir +command: + version: 6 + spokenForm: change value + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: value} + usePrePhraseSnapshot: true +initialState: + documentContents: | + %{ + :a => "lorem", + "b" => "ipsum", + 3 => "dolor" + } + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + marks: {} +finalState: + documentContents: | + %{ + :a => "lorem", + "b" => , + 3 => "dolor" + } + selections: + - anchor: {line: 2, character: 9} + active: {line: 2, character: 9} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckInsideFunk.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckInsideFunk.yml new file mode 100644 index 0000000000..31d5bb91ef --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckInsideFunk.yml @@ -0,0 +1,31 @@ +languageId: elixir +command: + version: 6 + spokenForm: chuck inside funk + action: + name: remove + target: + type: primitive + modifiers: + - {type: interiorOnly} + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + defmacro macro(x) do + quote do + [unquote(x)] + end + end + selections: + - anchor: {line: 2, character: 16} + active: {line: 2, character: 16} + marks: {} +finalState: + documentContents: |- + defmacro macro(x) do + end + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckInsideFunk2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckInsideFunk2.yml new file mode 100644 index 0000000000..adb8199602 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckInsideFunk2.yml @@ -0,0 +1,35 @@ +languageId: elixir +command: + version: 6 + spokenForm: chuck inside funk + action: + name: remove + target: + type: primitive + modifiers: + - {type: interiorOnly} + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + defmodule Mod do + defmacro macro(x) do + quote do + [unquote(x)] + end + end + end + selections: + - anchor: {line: 3, character: 18} + active: {line: 3, character: 18} + marks: {} +finalState: + documentContents: |- + defmodule Mod do + defmacro macro(x) do + end + end + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckItemTwo.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckItemTwo.yml new file mode 100644 index 0000000000..fd015cc97a --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckItemTwo.yml @@ -0,0 +1,27 @@ +languageId: elixir +command: + version: 6 + spokenForm: chuck item two + action: + name: remove + target: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: '2'} + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: "[1, 2, 3]" + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + marks: + default.2: + start: {line: 0, character: 4} + end: {line: 0, character: 5} +finalState: + documentContents: "[1, 3]" + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckItemTwo2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckItemTwo2.yml new file mode 100644 index 0000000000..7b3a3592f1 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckItemTwo2.yml @@ -0,0 +1,27 @@ +languageId: elixir +command: + version: 6 + spokenForm: chuck item two + action: + name: remove + target: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: '2'} + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: "%{a: 1, b: 2, c: 3}" + selections: + - anchor: {line: 0, character: 19} + active: {line: 0, character: 19} + marks: + default.2: + start: {line: 0, character: 11} + end: {line: 0, character: 12} +finalState: + documentContents: "%{a: 1, c: 3}" + selections: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 13} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckItemTwo3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckItemTwo3.yml new file mode 100644 index 0000000000..8595dd9012 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/elixir/chuckItemTwo3.yml @@ -0,0 +1,27 @@ +languageId: elixir +command: + version: 6 + spokenForm: chuck item two + action: + name: remove + target: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: '2'} + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: "%{\"a\" => 1, :b => 2, 3 => 3}" + selections: + - anchor: {line: 0, character: 28} + active: {line: 0, character: 28} + marks: + default.2: + start: {line: 0, character: 18} + end: {line: 0, character: 19} +finalState: + documentContents: "%{\"a\" => 1, 3 => 3}" + selections: + - anchor: {line: 0, character: 19} + active: {line: 0, character: 19} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/elixir/collectionItem.map.iteration.scope b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/elixir/collectionItem.map.iteration.scope new file mode 100644 index 0000000000..0fdb9cf4af --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/elixir/collectionItem.map.iteration.scope @@ -0,0 +1,12 @@ +defmodule DataStructures do + %{a: 1, b: 2} +end +--- + +[Range] = +[Domain] = 1:4-1:14 +0| defmodule DataStructures do + +1| %{a: 1, b: 2} + >----------< +2| end diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/elixir/collectionItem.map.scope b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/elixir/collectionItem.map.scope new file mode 100644 index 0000000000..5dfec3f6eb --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/elixir/collectionItem.map.scope @@ -0,0 +1,59 @@ +defmodule DataStructures do + %{a: 1, b: 2} +end +--- + +[#1 Content] = +[#1 Domain] = 1:4-1:8 +0| defmodule DataStructures do + +1| %{a: 1, b: 2} + >----< +2| end + + +[#1 Removal] = 1:4-1:10 +0| defmodule DataStructures do + +1| %{a: 1, b: 2} + >------< +2| end + + +[#1 Trailing delimiter] = 1:8-1:10 +0| defmodule DataStructures do + +1| %{a: 1, b: 2} + >--< +2| end + + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 1:10-1:14 +0| defmodule DataStructures do + +1| %{a: 1, b: 2} + >----< +2| end + + +[#2 Removal] = 1:8-1:14 +0| defmodule DataStructures do + +1| %{a: 1, b: 2} + >------< +2| end + + +[#2 Leading delimiter] = 1:8-1:10 +0| defmodule DataStructures do + +1| %{a: 1, b: 2} + >--< +2| end + + +[#2 Insertion delimiter] = ", " diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/elixir/collectionItem.map2.scope b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/elixir/collectionItem.map2.scope new file mode 100644 index 0000000000..a9b099331b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/elixir/collectionItem.map2.scope @@ -0,0 +1,98 @@ +defmodule DataStructures do + %{ + a: 1, + b: 2 + } +end +--- + +[#1 Content] = +[#1 Domain] = 2:4-2:8 +0| defmodule DataStructures do + +1| %{ + +2| a: 1, + >----< +3| b: 2 + +4| } + +5| end + + +[#1 Removal] = 2:4-3:4 +0| defmodule DataStructures do + +1| %{ + +2| a: 1, + >----- +3| b: 2 + ----< +4| } + +5| end + + +[#1 Trailing delimiter] = 2:8-3:4 +0| defmodule DataStructures do + +1| %{ + +2| a: 1, + >- +3| b: 2 + ----< +4| } + +5| end + + +[#1 Insertion delimiter] = ",\n" + + +[#2 Content] = +[#2 Domain] = 3:4-3:8 +0| defmodule DataStructures do + +1| %{ + +2| a: 1, + +3| b: 2 + >----< +4| } + +5| end + + +[#2 Removal] = 2:8-3:8 +0| defmodule DataStructures do + +1| %{ + +2| a: 1, + >- +3| b: 2 + --------< +4| } + +5| end + + +[#2 Leading delimiter] = 2:8-3:4 +0| defmodule DataStructures do + +1| %{ + +2| a: 1, + >- +3| b: 2 + ----< +4| } + +5| end + + +[#2 Insertion delimiter] = ",\n" diff --git a/resources/queries/elixir.scm b/resources/queries/elixir.scm new file mode 100644 index 0000000000..14566a195e --- /dev/null +++ b/resources/queries/elixir.scm @@ -0,0 +1,126 @@ +(comment) @comment @textFragment +(string) @string @textFragment +(anonymous_function) @anonymousFunction +(call + target: (_) @functionCallee +) @functionCall @functionCallee.domain + +;; Map +;; %{:a => "lorem", "b" => "ipsum", 3 => "dolor"} +( + (map_content + (_)? @_.leading.endOf + . + (binary_operator) @collectionItem + . + (_)? @_.trailing.startOf + ) @dummy + (#single-or-multi-line-delimiter! @collectionItem @dummy ", " ",\n") +) +(map_content + (binary_operator + left: (_) @collectionKey + right: (_) @value + ) @collectionKey.domain @value.domain +) + +;; Shorthand map syntax +;; %{a: "lorem", b: "ipsum"} +( + (map_content + (keywords + (_)? @_.leading.endOf + . + (pair) @collectionItem + . + (_)? @_.trailing.startOf + ) + (#single-or-multi-line-delimiter! @collectionItem @dummy ", " ",\n") + ) @dummy +) + +(map_content) @collectionItem.iteration @collectionKey.iteration @value.iteration + +(map_content + (keywords + (pair + key: (_) @collectionKey + value: (_) @value + ) @collectionKey.domain @value.domain + ) +) + +(call + target: (identifier) @_target + (#match? @_target "^(def|defp|defdelegate|defmacro|defmacrop|defn|defnp)$") + (arguments + [ + ; zero-arity functions with no parentheses + (identifier) @functionName + ; regular function clause + (call + target: (identifier) @functionName + ) + ; function clause with a guard clause + (binary_operator + left: (call + target: (identifier) @functionName + ) + operator: "when" + ) + ] + ) + (do_block) @namedFunction.interior + (#shrink-to-match! @namedFunction.interior "^do\\n?\\w*(?.*?\\n)\\s*end$") +) @namedFunction @functionName.domain + +;; def fun(), do: 1 +(call + target: (identifier) @_target + (#match? @_target "^(def|defp|defdelegate|defmacro|defmacrop|defn|defnp)$") + (arguments + (keywords + (pair + key: (_) @do_keyword + value: (_) @namedFunction.interior + ) + ) + (#match? @do_keyword "^do: $") + ) +) @namedFunction @functionName.domain + +;; defguard guard(term) when is_integer(term) and rem(term, 2) == 0 +(call + target: (identifier) @_target + (#match? @_target "^(defguard|defguardp)$") + (arguments + (binary_operator + left: (call + target: (identifier) @functionName + ) + operator: "when" + ) + ) +) @namedFunction @functionName.domain + +(call + target: (identifier) @_target + (#match? @_target "^(defmodule)$") + (arguments + (alias) @className + ) + (do_block + . + "do" @class.interior.start.endOf + "end" @class.interior.end.startOf + . + ) +) @class @className.domain + +(source) @className.iteration @class.iteration +(source) @statement.iteration +(source) @namedFunction.iteration @functionName.iteration +(call + target: (identifier) @_target + (#match? @_target "^(defmodule)$") +) @namedFunction.iteration @functionName.iteration