diff --git a/cursorless-talon/src/modifiers/modifiers.py b/cursorless-talon/src/modifiers/modifiers.py index d63351ab37..a15e24a821 100644 --- a/cursorless-talon/src/modifiers/modifiers.py +++ b/cursorless-talon/src/modifiers/modifiers.py @@ -27,7 +27,7 @@ def cursorless_simple_modifier(m) -> dict[str, str]: "", # funk, state, class, every funk "", # first past second word "", # next funk, 3 funks - "", # matching/pair [curly, round] + "", # DEPRECATED "left quad" / "right quad" ] modifiers = [ diff --git a/cursorless-talon/src/modifiers/scopes.py b/cursorless-talon/src/modifiers/scopes.py index ee1efb9b2c..032c0501e3 100644 --- a/cursorless-talon/src/modifiers/scopes.py +++ b/cursorless-talon/src/modifiers/scopes.py @@ -15,7 +15,10 @@ @mod.capture( - rule="{user.cursorless_scope_type} | | {user.cursorless_custom_regex_scope_type}" + rule="{user.cursorless_scope_type}" + " | " + " | " + " | {user.cursorless_custom_regex_scope_type}" ) def cursorless_scope_type(m) -> dict[str, str]: """Cursorless scope type singular""" @@ -24,16 +27,27 @@ def cursorless_scope_type(m) -> dict[str, str]: except AttributeError: pass + try: + return m.cursorless_surrounding_pair_scope_type + except AttributeError: + pass + try: return m.cursorless_glyph_scope_type except AttributeError: pass - return {"type": "customRegex", "regex": m.cursorless_custom_regex_scope_type} + return { + "type": "customRegex", + "regex": m.cursorless_custom_regex_scope_type, + } @mod.capture( - rule="{user.cursorless_scope_type_plural} | | {user.cursorless_custom_regex_scope_type_plural}" + rule="{user.cursorless_scope_type_plural}" + " | " + " | " + " | {user.cursorless_custom_regex_scope_type_plural}" ) def cursorless_scope_type_plural(m) -> dict[str, str]: """Cursorless scope type plural""" @@ -42,6 +56,11 @@ def cursorless_scope_type_plural(m) -> dict[str, str]: except AttributeError: pass + try: + return m.cursorless_surrounding_pair_scope_type_plural + except AttributeError: + pass + try: return m.cursorless_glyph_scope_type_plural except AttributeError: diff --git a/cursorless-talon/src/modifiers/surrounding_pair.py b/cursorless-talon/src/modifiers/surrounding_pair.py index 72e1cbddf0..f692eea9c4 100644 --- a/cursorless-talon/src/modifiers/surrounding_pair.py +++ b/cursorless-talon/src/modifiers/surrounding_pair.py @@ -22,6 +22,10 @@ "cursorless_surrounding_pair_scope_type", desc="Scope types that can function as surrounding pairs", ) +mod.list( + "cursorless_surrounding_pair_scope_type_plural", + desc="Plural form of scope types that can function as surrounding pairs", +) @mod.capture( @@ -30,29 +34,43 @@ "{user.cursorless_surrounding_pair_scope_type}" ) ) -def cursorless_surrounding_pair_scope_type(m) -> str: +def cursorless_surrounding_pair_scope_type(m) -> dict[str, str]: """Surrounding pair scope type""" try: - return m.cursorless_surrounding_pair_scope_type + delimiter = m.cursorless_surrounding_pair_scope_type except AttributeError: - return m.cursorless_selectable_paired_delimiter + delimiter = m.cursorless_selectable_paired_delimiter + return { + "type": "surroundingPair", + "delimiter": delimiter, + } @mod.capture( - rule="[{user.cursorless_delimiter_force_direction}] " + rule=( + " |" + "{user.cursorless_surrounding_pair_scope_type_plural}" + ) ) -def cursorless_surrounding_pair(m) -> dict[str, Any]: - """Expand to containing surrounding pair""" +def cursorless_surrounding_pair_scope_type_plural(m) -> dict[str, str]: + """Plural surrounding pair scope type""" try: - surrounding_pair_scope_type = m.cursorless_surrounding_pair_scope_type + delimiter = m.cursorless_surrounding_pair_scope_type_plural except AttributeError: - surrounding_pair_scope_type = "any" - - scope_type = { + delimiter = m.cursorless_selectable_paired_delimiter_plural + return { "type": "surroundingPair", - "delimiter": surrounding_pair_scope_type, + "delimiter": delimiter, } + +@mod.capture( + rule="{user.cursorless_delimiter_force_direction} " +) +def cursorless_surrounding_pair_force_direction(m) -> dict[str, Any]: + """DEPRECATED: Expand to containing surrounding pair""" + scope_type = m.cursorless_surrounding_pair_scope_type + with suppress(AttributeError): scope_type["forceDirection"] = m.cursorless_delimiter_force_direction diff --git a/cursorless-talon/src/paired_delimiter.py b/cursorless-talon/src/paired_delimiter.py index eeff2c94b4..3d80d655b2 100644 --- a/cursorless-talon/src/paired_delimiter.py +++ b/cursorless-talon/src/paired_delimiter.py @@ -15,6 +15,15 @@ desc="A paired delimiter that can be used as a scope type and as a wrapper", ) +mod.list( + "cursorless_selectable_only_paired_delimiter_plural", + desc="Plural form of a paired delimiter that can only be used as a scope type", +) +mod.list( + "cursorless_wrapper_selectable_paired_delimiter_plural", + desc="Plural form of a paired delimiter that can be used as a scope type and as a wrapper", +) + # Maps from the id we use in the spoken form csv to the delimiter strings paired_delimiters = { "curlyBrackets": ["{", "}"], @@ -58,3 +67,16 @@ def cursorless_selectable_paired_delimiter(m) -> str: return m.cursorless_selectable_only_paired_delimiter except AttributeError: return m.cursorless_wrapper_selectable_paired_delimiter + + +@mod.capture( + rule=( + "{user.cursorless_selectable_only_paired_delimiter_plural} |" + "{user.cursorless_wrapper_selectable_paired_delimiter_plural}" + ) +) +def cursorless_selectable_paired_delimiter_plural(m) -> str: + try: + return m.cursorless_selectable_only_paired_delimiter_plural + except AttributeError: + return m.cursorless_wrapper_selectable_paired_delimiter_plural diff --git a/cursorless-talon/src/spoken_forms.py b/cursorless-talon/src/spoken_forms.py index 1525655d83..218574c0d0 100644 --- a/cursorless-talon/src/spoken_forms.py +++ b/cursorless-talon/src/spoken_forms.py @@ -133,18 +133,29 @@ def handle_new_values(csv_name: str, values: list[SpokenFormEntry]): handle_csv("target_connectives.csv"), handle_csv("modifiers.csv"), handle_csv("positions.csv"), - handle_csv("paired_delimiters.csv"), + handle_csv( + "paired_delimiters.csv", + pluralize_lists=[ + "selectable_only_paired_delimiter", + "wrapper_selectable_paired_delimiter", + ], + ), handle_csv("special_marks.csv"), handle_csv("scope_visualizer.csv"), handle_csv("experimental/experimental_actions.csv"), handle_csv("experimental/miscellaneous.csv"), handle_csv( "modifier_scope_types.csv", - pluralize_lists=["scope_type", "glyph_scope_type"], + pluralize_lists=[ + "scope_type", + "glyph_scope_type", + "surrounding_pair_scope_type", + ], extra_allowed_values=[ "private.fieldAccess", "private.switchStatementSubject", "textFragment", + "disqualifyDelimiter", ], default_list_name="scope_type", ), diff --git a/data/fixtures/recorded/surroundingPair/changeNextRound.yml b/data/fixtures/recorded/surroundingPair/changeNextRound.yml new file mode 100644 index 0000000000..a1c3b65644 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changeNextRound.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change next round + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true +initialState: + documentContents: () () + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "() " + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} diff --git a/data/fixtures/recorded/surroundingPair/changeNextRound2.yml b/data/fixtures/recorded/surroundingPair/changeNextRound2.yml new file mode 100644 index 0000000000..2fb36df922 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changeNextRound2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change next round + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: false +initialState: + documentContents: () () + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: "() " + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} diff --git a/data/fixtures/recorded/surroundingPair/changeNextRound3.yml b/data/fixtures/recorded/surroundingPair/changeNextRound3.yml new file mode 100644 index 0000000000..f8dd9ba312 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changeNextRound3.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change next round + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true +initialState: + documentContents: (()) () + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: "(()) " + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} diff --git a/data/fixtures/recorded/surroundingPair/changeNextRound4.yml b/data/fixtures/recorded/surroundingPair/changeNextRound4.yml new file mode 100644 index 0000000000..92e38103ad --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changeNextRound4.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change next round + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true +initialState: + documentContents: (()) () + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: () () + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} diff --git a/data/fixtures/recorded/surroundingPair/changePreviousRound.yml b/data/fixtures/recorded/surroundingPair/changePreviousRound.yml new file mode 100644 index 0000000000..e8e6db6d94 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changePreviousRound.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change previous round + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: true +initialState: + documentContents: () () + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: " ()" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/changePreviousRound2.yml b/data/fixtures/recorded/surroundingPair/changePreviousRound2.yml new file mode 100644 index 0000000000..1e61befbfd --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changePreviousRound2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change previous round + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: true +initialState: + documentContents: () () + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: {} +finalState: + documentContents: " ()" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/changePreviousRound3.yml b/data/fixtures/recorded/surroundingPair/changePreviousRound3.yml new file mode 100644 index 0000000000..741c781076 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changePreviousRound3.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change previous round + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: true +initialState: + documentContents: (()) () + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + marks: {} +finalState: + documentContents: " ()" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/changeTwoRounds.yml b/data/fixtures/recorded/surroundingPair/changeTwoRounds.yml new file mode 100644 index 0000000000..a593328299 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changeTwoRounds.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change two rounds + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true +initialState: + documentContents: () () + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/changeTwoRounds2.yml b/data/fixtures/recorded/surroundingPair/changeTwoRounds2.yml new file mode 100644 index 0000000000..894fca48d1 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changeTwoRounds2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change two rounds + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true +initialState: + documentContents: () () + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/changeTwoRounds3.yml b/data/fixtures/recorded/surroundingPair/changeTwoRounds3.yml new file mode 100644 index 0000000000..5c30f4e86d --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changeTwoRounds3.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change two rounds + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true +initialState: + documentContents: (()) () + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/changeTwoRounds4.yml b/data/fixtures/recorded/surroundingPair/changeTwoRounds4.yml new file mode 100644 index 0000000000..2708ce9f34 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changeTwoRounds4.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change two rounds + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true +initialState: + documentContents: (()) () + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: ( + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} diff --git a/data/fixtures/recorded/surroundingPair/changeTwoRoundsBackward.yml b/data/fixtures/recorded/surroundingPair/changeTwoRoundsBackward.yml new file mode 100644 index 0000000000..510f4ae59c --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changeTwoRoundsBackward.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change two rounds backward + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 0 + length: 2 + direction: backward + usePrePhraseSnapshot: true +initialState: + documentContents: () () + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/changeTwoRoundsBackward2.yml b/data/fixtures/recorded/surroundingPair/changeTwoRoundsBackward2.yml new file mode 100644 index 0000000000..fa32b28619 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/changeTwoRoundsBackward2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change two rounds backward + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: parentheses} + offset: 0 + length: 2 + direction: backward + usePrePhraseSnapshot: true +initialState: + documentContents: () () + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/parseTree/cpp/changePair.yml b/data/fixtures/recorded/surroundingPair/parseTree/cpp/changePair.yml new file mode 100644 index 0000000000..c182c54556 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/parseTree/cpp/changePair.yml @@ -0,0 +1,25 @@ +languageId: cpp +command: + version: 7 + spokenForm: change pair + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: surroundingPair, delimiter: any} + usePrePhraseSnapshot: true +initialState: + documentContents: | + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + selections: + - anchor: {line: 0, character: 47} + active: {line: 0, character: 47} + marks: {} +finalState: + documentContents: | + #pragma clang diagnostic ignored + selections: + - anchor: {line: 0, character: 33} + active: {line: 0, character: 33} diff --git a/data/fixtures/recorded/surroundingPair/parseTree/python/changePair.yml b/data/fixtures/recorded/surroundingPair/parseTree/python/changePair.yml new file mode 100644 index 0000000000..4f71b9d999 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/parseTree/python/changePair.yml @@ -0,0 +1,29 @@ +languageId: python +command: + version: 7 + spokenForm: change pair + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: surroundingPair, delimiter: any} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + [ + "character" + ] + selections: + - anchor: {line: 1, character: 9} + active: {line: 1, character: 9} + marks: {} +finalState: + documentContents: |- + [ + + ] + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeOutside15.yml b/data/fixtures/recorded/surroundingPair/parseTree/python/changePair2.yml similarity index 51% rename from data/fixtures/recorded/surroundingPair/textual/takeOutside15.yml rename to data/fixtures/recorded/surroundingPair/parseTree/python/changePair2.yml index b42abaaf0d..99faeae3d1 100644 --- a/data/fixtures/recorded/surroundingPair/textual/takeOutside15.yml +++ b/data/fixtures/recorded/surroundingPair/parseTree/python/changePair2.yml @@ -1,22 +1,23 @@ -languageId: plaintext +languageId: python command: - version: 5 + version: 7 spokenForm: change pair - action: {name: clearAndSetSelection} - targets: - - type: primitive + action: + name: clearAndSetSelection + target: + type: primitive modifiers: - type: containingScope scopeType: {type: surroundingPair, delimiter: any} - usePrePhraseSnapshot: false + usePrePhraseSnapshot: true initialState: - documentContents: ( [ ) ] + documentContents: "\" r\"" selections: - - anchor: {line: 0, character: 7} - active: {line: 0, character: 7} + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} marks: {} finalState: - documentContents: " ]" + documentContents: "" selections: - anchor: {line: 0, character: 0} active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/parseTree/python/clearQuad2.yml b/data/fixtures/recorded/surroundingPair/parseTree/python/clearQuad2.yml index 2c55b58394..eb17bde42f 100644 --- a/data/fixtures/recorded/surroundingPair/parseTree/python/clearQuad2.yml +++ b/data/fixtures/recorded/surroundingPair/parseTree/python/clearQuad2.yml @@ -1,13 +1,13 @@ languageId: python command: version: 5 - spokenForm: change quad + spokenForm: change string action: {name: clearAndSetSelection} targets: - type: primitive modifiers: - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes} + scopeType: {type: surroundingPair, delimiter: string} usePrePhraseSnapshot: false initialState: documentContents: |- diff --git a/data/fixtures/recorded/surroundingPair/parseTreeParity/takeOutside15.yml b/data/fixtures/recorded/surroundingPair/parseTree/typescript/changePair.yml similarity index 54% rename from data/fixtures/recorded/surroundingPair/parseTreeParity/takeOutside15.yml rename to data/fixtures/recorded/surroundingPair/parseTree/typescript/changePair.yml index 899abcb356..f9187f9fa4 100644 --- a/data/fixtures/recorded/surroundingPair/parseTreeParity/takeOutside15.yml +++ b/data/fixtures/recorded/surroundingPair/parseTree/typescript/changePair.yml @@ -1,22 +1,26 @@ languageId: typescript command: - version: 5 + version: 7 spokenForm: change pair - action: {name: clearAndSetSelection} - targets: - - type: primitive + action: + name: clearAndSetSelection + target: + type: primitive modifiers: - type: containingScope scopeType: {type: surroundingPair, delimiter: any} - usePrePhraseSnapshot: false + usePrePhraseSnapshot: true initialState: - documentContents: ( [ ) ] + documentContents: |- + ( + // ) + ) selections: - - anchor: {line: 0, character: 7} - active: {line: 0, character: 7} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} marks: {} finalState: - documentContents: " ]" + documentContents: "" selections: - anchor: {line: 0, character: 0} active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/parseTree/typescript/changeRound.yml b/data/fixtures/recorded/surroundingPair/parseTree/typescript/changeRound.yml new file mode 100644 index 0000000000..da43b6952b --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/parseTree/typescript/changeRound.yml @@ -0,0 +1,23 @@ +languageId: typescript +command: + version: 7 + spokenForm: change round + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: surroundingPair, delimiter: parentheses} + usePrePhraseSnapshot: true +initialState: + documentContents: ("(") + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/parseTreeParity/takeOutside19.yml b/data/fixtures/recorded/surroundingPair/parseTreeParity/takeOutside19.yml deleted file mode 100644 index 81c4299062..0000000000 --- a/data/fixtures/recorded/surroundingPair/parseTreeParity/takeOutside19.yml +++ /dev/null @@ -1,22 +0,0 @@ -languageId: typescript -command: - version: 5 - spokenForm: change pair - action: {name: clearAndSetSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: any} - usePrePhraseSnapshot: false -initialState: - documentContents: "{ ( } ] )" - selections: - - anchor: {line: 0, character: 8} - active: {line: 0, character: 8} - marks: {} -finalState: - documentContents: " ] )" - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/textual/changeNextPair.yml b/data/fixtures/recorded/surroundingPair/textual/changeNextPair.yml new file mode 100644 index 0000000000..fb0e91d906 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/textual/changeNextPair.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change next pair + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: any} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true +initialState: + documentContents: () () + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: {} +finalState: + documentContents: "() " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/surroundingPair/textual/changePair.yml b/data/fixtures/recorded/surroundingPair/textual/changePair.yml new file mode 100644 index 0000000000..2f105294a8 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/textual/changePair.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change pair + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: surroundingPair, delimiter: any} + usePrePhraseSnapshot: true +initialState: + documentContents: "[]()" + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: "[]" + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} diff --git a/data/fixtures/recorded/surroundingPair/textual/changePairBackward.yml b/data/fixtures/recorded/surroundingPair/textual/changePairBackward.yml new file mode 100644 index 0000000000..e0800a7146 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/textual/changePairBackward.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change pair backward + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: any} + offset: 0 + length: 1 + direction: backward + usePrePhraseSnapshot: true +initialState: + documentContents: "[]()" + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: () + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/textual/changePreviousPair.yml b/data/fixtures/recorded/surroundingPair/textual/changePreviousPair.yml new file mode 100644 index 0000000000..769d872c66 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/textual/changePreviousPair.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change previous pair + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: any} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: true +initialState: + documentContents: () () + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: {} +finalState: + documentContents: " ()" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/textual/changeTwoPairs.yml b/data/fixtures/recorded/surroundingPair/textual/changeTwoPairs.yml new file mode 100644 index 0000000000..02c77a6d2c --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/textual/changeTwoPairs.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change two pairs + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: any} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true +initialState: + documentContents: ()() + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad.yml b/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad.yml index 4ff5f97d76..0731799bc3 100644 --- a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad.yml +++ b/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad.yml @@ -17,10 +17,4 @@ initialState: - anchor: {line: 0, character: 26} active: {line: 0, character: 26} marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 0, character: 21} - active: {line: 1, character: 16} +thrownError: {name: NoContainingScopeError} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad2.yml b/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad2.yml deleted file mode 100644 index 08ba498272..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad2.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take left quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: left} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 15} - active: {line: 1, character: 15} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 0, character: 21} - active: {line: 1, character: 16} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad3.yml b/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad3.yml deleted file mode 100644 index b9b2b37ec9..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad3.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take left quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: left} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 16} - active: {line: 1, character: 16} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 0, character: 21} - active: {line: 1, character: 16} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad4.yml b/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad4.yml deleted file mode 100644 index bc6efdcdd1..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad4.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take left quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: left} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 12} - active: {line: 1, character: 12} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 0, character: 21} - active: {line: 1, character: 16} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad5.yml b/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad5.yml deleted file mode 100644 index 6b0b655951..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad5.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take left quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: left} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 26} - active: {line: 1, character: 26} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 22} - active: {line: 1, character: 32} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad6.yml b/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad6.yml deleted file mode 100644 index 30965511ba..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad6.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take left quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: left} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 31} - active: {line: 1, character: 31} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 22} - active: {line: 1, character: 32} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad7.yml b/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad7.yml deleted file mode 100644 index c4a05ea1c4..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeLeftQuad7.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take left quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: left} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 32} - active: {line: 1, character: 32} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 22} - active: {line: 1, character: 32} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeOutside19.yml b/data/fixtures/recorded/surroundingPair/textual/takeOutside19.yml deleted file mode 100644 index 2b96def99f..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeOutside19.yml +++ /dev/null @@ -1,22 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: change pair - action: {name: clearAndSetSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: any} - usePrePhraseSnapshot: false -initialState: - documentContents: "{ ( } ] )" - selections: - - anchor: {line: 0, character: 8} - active: {line: 0, character: 8} - marks: {} -finalState: - documentContents: " ] )" - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeOutside27.yml b/data/fixtures/recorded/surroundingPair/textual/takeOutside27.yml index 5a0f15cefe..1d6d48b850 100644 --- a/data/fixtures/recorded/surroundingPair/textual/takeOutside27.yml +++ b/data/fixtures/recorded/surroundingPair/textual/takeOutside27.yml @@ -16,7 +16,7 @@ initialState: active: {line: 0, character: 18} marks: {} finalState: - documentContents: "" + documentContents: () selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad.yml b/data/fixtures/recorded/surroundingPair/textual/takeRightQuad.yml index c80275ec45..012f1a1020 100644 --- a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad.yml +++ b/data/fixtures/recorded/surroundingPair/textual/takeRightQuad.yml @@ -17,10 +17,4 @@ initialState: - anchor: {line: 0, character: 21} active: {line: 0, character: 21} marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 0, character: 21} - active: {line: 1, character: 16} +thrownError: {name: NoContainingScopeError} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad2.yml b/data/fixtures/recorded/surroundingPair/textual/takeRightQuad2.yml deleted file mode 100644 index 34a54836f4..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad2.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take right quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: right} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 0, character: 22} - active: {line: 0, character: 22} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 0, character: 21} - active: {line: 1, character: 16} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad3.yml b/data/fixtures/recorded/surroundingPair/textual/takeRightQuad3.yml deleted file mode 100644 index 15f5800b43..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad3.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take right quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: right} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 0, character: 26} - active: {line: 0, character: 26} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 0, character: 21} - active: {line: 1, character: 16} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad4.yml b/data/fixtures/recorded/surroundingPair/textual/takeRightQuad4.yml deleted file mode 100644 index bbc7f7a2e5..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad4.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take right quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: right} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 22} - active: {line: 1, character: 22} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 22} - active: {line: 1, character: 32} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad5.yml b/data/fixtures/recorded/surroundingPair/textual/takeRightQuad5.yml deleted file mode 100644 index df6f6d37e9..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad5.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take right quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: right} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 23} - active: {line: 1, character: 23} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 22} - active: {line: 1, character: 32} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad6.yml b/data/fixtures/recorded/surroundingPair/textual/takeRightQuad6.yml deleted file mode 100644 index c4bae7b438..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad6.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take right quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: right} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 12} - active: {line: 1, character: 12} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 0, character: 21} - active: {line: 1, character: 16} diff --git a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad7.yml b/data/fixtures/recorded/surroundingPair/textual/takeRightQuad7.yml deleted file mode 100644 index 8ab55a0217..0000000000 --- a/data/fixtures/recorded/surroundingPair/textual/takeRightQuad7.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - version: 5 - spokenForm: take right quad - action: {name: setSelection} - targets: - - type: primitive - modifiers: - - type: containingScope - scopeType: {type: surroundingPair, delimiter: doubleQuotes, forceDirection: right} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 26} - active: {line: 1, character: 26} - marks: {} -finalState: - documentContents: |- - hello world whatever "testing testing testing - this is another" test "whatever" whatever - selections: - - anchor: {line: 1, character: 22} - active: {line: 1, character: 32} diff --git a/data/fixtures/scopes/c/disqualifyDelimiter.scope b/data/fixtures/scopes/c/disqualifyDelimiter.scope new file mode 100644 index 0000000000..7664c17577 --- /dev/null +++ b/data/fixtures/scopes/c/disqualifyDelimiter.scope @@ -0,0 +1,45 @@ +1 < 2; +1 > 2; +1 <= 2; +1 >= 2; +a << 2; +a >> 2; +a <<= 2; +a >>= 2; +a->b +--- +[#1 Content] = 0:2-0:3 + >-< +0| 1 < 2; + +[#2 Content] = 1:2-1:3 + >-< +1| 1 > 2; + +[#3 Content] = 2:2-2:4 + >--< +2| 1 <= 2; + +[#4 Content] = 3:2-3:4 + >--< +3| 1 >= 2; + +[#5 Content] = 4:2-4:4 + >--< +4| a << 2; + +[#6 Content] = 5:2-5:4 + >--< +5| a >> 2; + +[#7 Content] = 6:2-6:5 + >---< +6| a <<= 2; + +[#8 Content] = 7:2-7:5 + >---< +7| a >>= 2; + +[#9 Content] = 8:1-8:3 + >--< +8| a->b diff --git a/data/fixtures/scopes/cpp/disqualifyDelimiter.scope b/data/fixtures/scopes/cpp/disqualifyDelimiter.scope new file mode 100644 index 0000000000..9097f5bbeb --- /dev/null +++ b/data/fixtures/scopes/cpp/disqualifyDelimiter.scope @@ -0,0 +1,5 @@ +auto max(int a, int b) -> int; +--- +[Content] = 0:23-0:25 + >--< +0| auto max(int a, int b) -> int; diff --git a/data/fixtures/scopes/csharp/disqualifyDelimiter.scope b/data/fixtures/scopes/csharp/disqualifyDelimiter.scope new file mode 100644 index 0000000000..3cfe7b0fe1 --- /dev/null +++ b/data/fixtures/scopes/csharp/disqualifyDelimiter.scope @@ -0,0 +1,50 @@ +1 < 2; +1 > 2; +1 <= 2; +1 >= 2; +a << 2; +a >> 2; +a <<= 2; +a >>= 2; +foo->bar; +() => 2; +--- +[#1 Content] = 0:2-0:3 + >-< +0| 1 < 2; + +[#2 Content] = 1:2-1:3 + >-< +1| 1 > 2; + +[#3 Content] = 2:2-2:4 + >--< +2| 1 <= 2; + +[#4 Content] = 3:2-3:4 + >--< +3| 1 >= 2; + +[#5 Content] = 4:2-4:4 + >--< +4| a << 2; + +[#6 Content] = 5:2-5:4 + >--< +5| a >> 2; + +[#7 Content] = 6:2-6:5 + >---< +6| a <<= 2; + +[#8 Content] = 7:2-7:5 + >---< +7| a >>= 2; + +[#9 Content] = 8:3-8:5 + >--< +8| foo->bar; + +[#10 Content] = 9:3-9:5 + >--< +9| () => 2; diff --git a/data/fixtures/scopes/css/disqualifyDelimiter.scope b/data/fixtures/scopes/css/disqualifyDelimiter.scope new file mode 100644 index 0000000000..dbc80ec255 --- /dev/null +++ b/data/fixtures/scopes/css/disqualifyDelimiter.scope @@ -0,0 +1,5 @@ +div > div {} +--- +[Content] = 0:4-0:5 + >-< +0| div > div {} diff --git a/data/fixtures/scopes/go/disqualifyDelimiter.scope b/data/fixtures/scopes/go/disqualifyDelimiter.scope new file mode 100644 index 0000000000..195db899c3 --- /dev/null +++ b/data/fixtures/scopes/go/disqualifyDelimiter.scope @@ -0,0 +1,50 @@ +1 < 2 +1 > 2 +1 <= 2 +1 >= 2 +a << 2 +a >> 2 +a <<= 2 +a >>= 2 +ch <- 42 +msg := <- ch +--- +[#1 Content] = 0:2-0:3 + >-< +0| 1 < 2 + +[#2 Content] = 1:2-1:3 + >-< +1| 1 > 2 + +[#3 Content] = 2:2-2:4 + >--< +2| 1 <= 2 + +[#4 Content] = 3:2-3:4 + >--< +3| 1 >= 2 + +[#5 Content] = 4:2-4:4 + >--< +4| a << 2 + +[#6 Content] = 5:2-5:4 + >--< +5| a >> 2 + +[#7 Content] = 6:2-6:5 + >---< +6| a <<= 2 + +[#8 Content] = 7:2-7:5 + >---< +7| a >>= 2 + +[#9 Content] = 8:3-8:5 + >--< +8| ch <- 42 + +[#10 Content] = 9:7-9:9 + >--< +9| msg := <- ch diff --git a/data/fixtures/scopes/java/disqualifyDelimiter.scope b/data/fixtures/scopes/java/disqualifyDelimiter.scope new file mode 100644 index 0000000000..a0005ea65d --- /dev/null +++ b/data/fixtures/scopes/java/disqualifyDelimiter.scope @@ -0,0 +1,60 @@ +1 < 2; +1 > 2; +1 <= 2; +1 >= 2; +a << 2; +a >> 2; +a <<= 2; +a >>= 2; +a >>> 2; +a >>>= 2; +() -> 2; +switch () { case "foo" -> 1 } +--- +[#1 Content] = 0:2-0:3 + >-< +0| 1 < 2; + +[#2 Content] = 1:2-1:3 + >-< +1| 1 > 2; + +[#3 Content] = 2:2-2:4 + >--< +2| 1 <= 2; + +[#4 Content] = 3:2-3:4 + >--< +3| 1 >= 2; + +[#5 Content] = 4:2-4:4 + >--< +4| a << 2; + +[#6 Content] = 5:2-5:4 + >--< +5| a >> 2; + +[#7 Content] = 6:2-6:5 + >---< +6| a <<= 2; + +[#8 Content] = 7:2-7:5 + >---< +7| a >>= 2; + +[#9 Content] = 8:2-8:5 + >---< +8| a >>> 2; + +[#10 Content] = 9:2-9:6 + >----< +9| a >>>= 2; + +[#11 Content] = 10:3-10:5 + >--< +10| () -> 2; + +[#12 Content] = 11:23-11:25 + >--< +11| switch () { case "foo" -> 1 } diff --git a/data/fixtures/scopes/javascript.core/disqualifyDelimiter.scope b/data/fixtures/scopes/javascript.core/disqualifyDelimiter.scope new file mode 100644 index 0000000000..be4cde6c69 --- /dev/null +++ b/data/fixtures/scopes/javascript.core/disqualifyDelimiter.scope @@ -0,0 +1,55 @@ +1 < 2; +1 > 2; +1 <= 2; +1 >= 2; +a << 2; +a >> 2; +a <<= 2; +a >>= 2; +a >>> 2; +a >>>= 2; +() => 2; +--- +[#1 Content] = 0:2-0:3 + >-< +0| 1 < 2; + +[#2 Content] = 1:2-1:3 + >-< +1| 1 > 2; + +[#3 Content] = 2:2-2:4 + >--< +2| 1 <= 2; + +[#4 Content] = 3:2-3:4 + >--< +3| 1 >= 2; + +[#5 Content] = 4:2-4:4 + >--< +4| a << 2; + +[#6 Content] = 5:2-5:4 + >--< +5| a >> 2; + +[#7 Content] = 6:2-6:5 + >---< +6| a <<= 2; + +[#8 Content] = 7:2-7:5 + >---< +7| a >>= 2; + +[#9 Content] = 8:2-8:5 + >---< +8| a >>> 2; + +[#10 Content] = 9:2-9:6 + >----< +9| a >>>= 2; + +[#11 Content] = 10:3-10:5 + >--< +10| () => 2; diff --git a/data/fixtures/scopes/latex/disqualifyDelimiter.scope b/data/fixtures/scopes/latex/disqualifyDelimiter.scope new file mode 100644 index 0000000000..489ca6ced4 --- /dev/null +++ b/data/fixtures/scopes/latex/disqualifyDelimiter.scope @@ -0,0 +1,10 @@ +1 < 2 +1 > 2 +--- +[#1 Content] = 0:2-0:3 + >-< +0| 1 < 2 + +[#2 Content] = 1:2-1:3 + >-< +1| 1 > 2 diff --git a/data/fixtures/scopes/lua/disqualifyDelimiter.scope b/data/fixtures/scopes/lua/disqualifyDelimiter.scope new file mode 100644 index 0000000000..2714290df5 --- /dev/null +++ b/data/fixtures/scopes/lua/disqualifyDelimiter.scope @@ -0,0 +1,30 @@ +a = 1 < 2 +a = 1 > 2 +a = 1 <= 2 +a = 1 >= 2 +a = 1 << 2 +a = 1 >> 2 +--- +[#1 Content] = 0:6-0:7 + >-< +0| a = 1 < 2 + +[#2 Content] = 1:6-1:7 + >-< +1| a = 1 > 2 + +[#3 Content] = 2:6-2:8 + >--< +2| a = 1 <= 2 + +[#4 Content] = 3:6-3:8 + >--< +3| a = 1 >= 2 + +[#5 Content] = 4:6-4:8 + >--< +4| a = 1 << 2 + +[#6 Content] = 5:6-5:8 + >--< +5| a = 1 >> 2 diff --git a/data/fixtures/scopes/php/disqualifyDelimiter.scope b/data/fixtures/scopes/php/disqualifyDelimiter.scope new file mode 100644 index 0000000000..0bac13f328 --- /dev/null +++ b/data/fixtures/scopes/php/disqualifyDelimiter.scope @@ -0,0 +1,76 @@ + 2; +1 <= 2; +1 >= 2; +a << 2; +a >> 2; +a <<= 2; +a >>= 2; +['first' => 1]; +$a = <<< EOT; +$a = <<< 'TEXT'; +foo->bar; +foo->bar(); +foo?->bar; +foo?->bar(); +--- +[#1 Content] = 1:2-1:3 + >-< +1| 1 < 2; + +[#2 Content] = 2:2-2:3 + >-< +2| 1 > 2; + +[#3 Content] = 3:2-3:4 + >--< +3| 1 <= 2; + +[#4 Content] = 4:2-4:4 + >--< +4| 1 >= 2; + +[#5 Content] = 5:2-5:4 + >--< +5| a << 2; + +[#6 Content] = 6:2-6:4 + >--< +6| a >> 2; + +[#7 Content] = 7:2-7:4 + >--< +7| a <<= 2; + +[#8 Content] = 8:2-8:4 + >--< +8| a >>= 2; + +[#9 Content] = 9:9-9:11 + >--< +9| ['first' => 1]; + +[#10 Content] = 10:5-10:8 + >---< +10| $a = <<< EOT; + +[#11 Content] = 11:5-11:8 + >---< +11| $a = <<< 'TEXT'; + +[#12 Content] = 12:3-12:5 + >--< +12| foo->bar; + +[#13 Content] = 13:3-13:5 + >--< +13| foo->bar(); + +[#14 Content] = 14:3-14:6 + >---< +14| foo?->bar; + +[#15 Content] = 15:3-15:6 + >---< +15| foo?->bar(); diff --git a/data/fixtures/scopes/python/disqualifyDelimiter.scope b/data/fixtures/scopes/python/disqualifyDelimiter.scope new file mode 100644 index 0000000000..d2e9a6cedb --- /dev/null +++ b/data/fixtures/scopes/python/disqualifyDelimiter.scope @@ -0,0 +1,45 @@ +1 < 2 +1 > 2 +1 <= 2 +1 >= 2 +a << 2 +a >> 2 +a <<= 2 +a >>= 2 +def foo() -> int: +--- +[#1 Content] = 0:2-0:3 + >-< +0| 1 < 2 + +[#2 Content] = 1:2-1:3 + >-< +1| 1 > 2 + +[#3 Content] = 2:2-2:4 + >--< +2| 1 <= 2 + +[#4 Content] = 3:2-3:4 + >--< +3| 1 >= 2 + +[#5 Content] = 4:2-4:4 + >--< +4| a << 2 + +[#6 Content] = 5:2-5:4 + >--< +5| a >> 2 + +[#7 Content] = 6:2-6:5 + >---< +6| a <<= 2 + +[#8 Content] = 7:2-7:5 + >---< +7| a >>= 2 + +[#9 Content] = 8:10-8:12 + >--< +8| def foo() -> int: diff --git a/data/fixtures/scopes/ruby/disqualifyDelimiter.scope b/data/fixtures/scopes/ruby/disqualifyDelimiter.scope new file mode 100644 index 0000000000..6875157b68 --- /dev/null +++ b/data/fixtures/scopes/ruby/disqualifyDelimiter.scope @@ -0,0 +1,50 @@ +1 < 2 +1 > 2 +1 <= 2 +1 >= 2 +a << 2 +a >> 2 +a <<= 2 +a >>= 2 +a = { :b => 2 } +case 85 when 0 then => "Fail" end +--- +[#1 Content] = 0:2-0:3 + >-< +0| 1 < 2 + +[#2 Content] = 1:2-1:3 + >-< +1| 1 > 2 + +[#3 Content] = 2:2-2:4 + >--< +2| 1 <= 2 + +[#4 Content] = 3:2-3:4 + >--< +3| 1 >= 2 + +[#5 Content] = 4:2-4:4 + >--< +4| a << 2 + +[#6 Content] = 5:2-5:4 + >--< +5| a >> 2 + +[#7 Content] = 6:2-6:5 + >---< +6| a <<= 2 + +[#8 Content] = 7:2-7:5 + >---< +7| a >>= 2 + +[#9 Content] = 8:9-8:11 + >--< +8| a = { :b => 2 } + +[#10 Content] = 9:20-9:22 + >--< +9| case 85 when 0 then => "Fail" end diff --git a/data/fixtures/scopes/rust/disqualifyDelimiter.scope b/data/fixtures/scopes/rust/disqualifyDelimiter.scope new file mode 100644 index 0000000000..0d781a8fb4 --- /dev/null +++ b/data/fixtures/scopes/rust/disqualifyDelimiter.scope @@ -0,0 +1,55 @@ +1 < 2; +1 > 2; +1 <= 2; +1 >= 2; +a << 2; +a >> 2; +a <<= 2; +a >>= 2; +fn foo() -> string {} +match number { 0 => "fail" } +macro_rules! my_expr { ($x:expr) => {}; } +--- +[#1 Content] = 0:2-0:3 + >-< +0| 1 < 2; + +[#2 Content] = 1:2-1:3 + >-< +1| 1 > 2; + +[#3 Content] = 2:2-2:4 + >--< +2| 1 <= 2; + +[#4 Content] = 3:2-3:4 + >--< +3| 1 >= 2; + +[#5 Content] = 4:2-4:4 + >--< +4| a << 2; + +[#6 Content] = 5:2-5:4 + >--< +5| a >> 2; + +[#7 Content] = 6:2-6:5 + >---< +6| a <<= 2; + +[#8 Content] = 7:2-7:5 + >---< +7| a >>= 2; + +[#9 Content] = 8:9-8:11 + >--< +8| fn foo() -> string {} + +[#10 Content] = 9:17-9:19 + >--< +9| match number { 0 => "fail" } + +[#11 Content] = 10:33-10:35 + >--< +10| macro_rules! my_expr { ($x:expr) => {}; } diff --git a/data/fixtures/scopes/scala/disqualifyDelimiter.scope b/data/fixtures/scopes/scala/disqualifyDelimiter.scope new file mode 100644 index 0000000000..6c66bb054a --- /dev/null +++ b/data/fixtures/scopes/scala/disqualifyDelimiter.scope @@ -0,0 +1,75 @@ +val minValue = if (1 < 2) a else b +val minValue = if (1 > 2) a else b +val minValue = if (1 <= 2) a else b +val minValue = if (1 >= 2) a else b +val minValue = if (a << 2) a else b +val minValue = if (a >> 2) a else b +val minValue = if (a <<= 2) a else b +val minValue = if (a >>= 2) a else b +for (n <- numbers) yield n +def function[T <% String](x: T) = {} +def method[T <: BaseType](param: T) = {} +def method[T >: BaseType](param: T) = {} +val foo = (x: Int) => x +val func: (Int, Int) => Int = foo +value match { case 0 => "fail" } +--- +[#1 Content] = 0:21-0:22 + >-< +0| val minValue = if (1 < 2) a else b + +[#2 Content] = 1:21-1:22 + >-< +1| val minValue = if (1 > 2) a else b + +[#3 Content] = 2:21-2:23 + >--< +2| val minValue = if (1 <= 2) a else b + +[#4 Content] = 3:21-3:23 + >--< +3| val minValue = if (1 >= 2) a else b + +[#5 Content] = 4:21-4:23 + >--< +4| val minValue = if (a << 2) a else b + +[#6 Content] = 5:21-5:23 + >--< +5| val minValue = if (a >> 2) a else b + +[#7 Content] = 6:21-6:24 + >---< +6| val minValue = if (a <<= 2) a else b + +[#8 Content] = 7:21-7:24 + >---< +7| val minValue = if (a >>= 2) a else b + +[#9 Content] = 8:7-8:9 + >--< +8| for (n <- numbers) yield n + +[#10 Content] = 9:15-9:17 + >--< +9| def function[T <% String](x: T) = {} + +[#11 Content] = 10:13-10:15 + >--< +10| def method[T <: BaseType](param: T) = {} + +[#12 Content] = 11:13-11:15 + >--< +11| def method[T >: BaseType](param: T) = {} + +[#13 Content] = 12:19-12:21 + >--< +12| val foo = (x: Int) => x + +[#14 Content] = 13:21-13:23 + >--< +13| val func: (Int, Int) => Int = foo + +[#15 Content] = 14:21-14:23 + >--< +14| value match { case 0 => "fail" } diff --git a/data/fixtures/scopes/scss/disqualifyDelimiter.scope b/data/fixtures/scopes/scss/disqualifyDelimiter.scope new file mode 100644 index 0000000000..cff0af49a4 --- /dev/null +++ b/data/fixtures/scopes/scss/disqualifyDelimiter.scope @@ -0,0 +1,22 @@ +* { + @if $container-width < $base-width {} + @if $container-width <= $base-width {} + @if $container-width > $base-width {} + @if $container-width >= $base-width {} +} +--- +[#1 Content] = 1:23-1:24 + >-< +1| @if $container-width < $base-width {} + +[#2 Content] = 2:23-2:25 + >--< +2| @if $container-width <= $base-width {} + +[#3 Content] = 3:23-3:24 + >-< +3| @if $container-width > $base-width {} + +[#4 Content] = 4:23-4:25 + >--< +4| @if $container-width >= $base-width {} diff --git a/data/fixtures/scopes/textual/surroundingPair.iteration.scope b/data/fixtures/scopes/textual/surroundingPair.iteration.scope new file mode 100644 index 0000000000..98176cda69 --- /dev/null +++ b/data/fixtures/scopes/textual/surroundingPair.iteration.scope @@ -0,0 +1,21 @@ + +( ) + +--- + +[#1 Range] = +[#1 Domain] = 0:0-0:0 + >< +0| + + +[#2 Range] = +[#2 Domain] = 1:0-1:3 + >---< +1| ( ) + + +[#3 Range] = +[#3 Domain] = 2:0-2:0 + >< +2| diff --git a/data/fixtures/scopes/textual/surroundingPair.iteration2.scope b/data/fixtures/scopes/textual/surroundingPair.iteration2.scope new file mode 100644 index 0000000000..5fc343d946 --- /dev/null +++ b/data/fixtures/scopes/textual/surroundingPair.iteration2.scope @@ -0,0 +1,21 @@ + +hello + +--- + +[#1 Range] = +[#1 Domain] = 0:0-0:0 + >< +0| + + +[#2 Range] = +[#2 Domain] = 1:0-1:5 + >-----< +1| hello + + +[#3 Range] = +[#3 Domain] = 2:0-2:0 + >< +2| diff --git a/data/fixtures/scopes/textual/surroundingPair.scope b/data/fixtures/scopes/textual/surroundingPair.scope new file mode 100644 index 0000000000..c294a63df7 --- /dev/null +++ b/data/fixtures/scopes/textual/surroundingPair.scope @@ -0,0 +1,28 @@ +( ) +--- + +[Content] = +[Removal] = +[Domain] = 0:0-0:3 + >---< +0| ( ) + +[Interior] = 0:1-0:2 + >-< +0| ( ) + +[Boundary L: Content] = 0:0-0:1 + >-< +0| ( ) +[Boundary L: Removal] = 0:0-0:2 + >--< +0| ( ) + +[Boundary R: Content] = 0:2-0:3 + >-< +0| ( ) +[Boundary R: Removal] = 0:1-0:3 + >--< +0| ( ) + +[Insertion delimiter] = " " diff --git a/data/fixtures/scopes/textual/surroundingPair2.scope b/data/fixtures/scopes/textual/surroundingPair2.scope new file mode 100644 index 0000000000..839a6471fe --- /dev/null +++ b/data/fixtures/scopes/textual/surroundingPair2.scope @@ -0,0 +1,24 @@ +" +"hello" +" +--- + +[Content] = +[Removal] = +[Domain] = 1:0-1:7 + >-------< +1| "hello" + +[Interior] = 1:1-1:6 + >-----< +1| "hello" + +[Boundary L] = 1:0-1:1 + >-< +1| "hello" + +[Boundary R] = 1:6-1:7 + >-< +1| "hello" + +[Insertion delimiter] = " " diff --git a/data/fixtures/scopes/textual/surroundingPair3.scope b/data/fixtures/scopes/textual/surroundingPair3.scope new file mode 100644 index 0000000000..b9245b59ca --- /dev/null +++ b/data/fixtures/scopes/textual/surroundingPair3.scope @@ -0,0 +1,81 @@ +(()) () +--- + +[#1 Content] = +[#1 Domain] = 0:0-0:4 + >----< +0| (()) () + +[#1 Removal] = 0:0-0:5 + >-----< +0| (()) () + +[#1 Trailing delimiter] = 0:4-0:5 + >-< +0| (()) () + +[#1 Interior] = 0:1-0:3 + >--< +0| (()) () + +[#1 Boundary L] = 0:0-0:1 + >-< +0| (()) () + +[#1 Boundary R] = 0:3-0:4 + >-< +0| (()) () + +[#1 Insertion delimiter] = " " + + +[#2 Content] = +[#2 Removal] = +[#2 Domain] = 0:1-0:3 + >--< +0| (()) () + +[#2 Interior] = 0:2-0:2 + >< +0| (()) () + +[#2 Boundary L] = 0:1-0:2 + >-< +0| (()) () + +[#2 Boundary R] = 0:2-0:3 + >-< +0| (()) () + +[#2 Insertion delimiter] = " " + + +[#3 Content] = +[#3 Domain] = 0:5-0:7 + >--< +0| (()) () + +[#3 Removal] = 0:4-0:7 + >---< +0| (()) () + +[#3 Leading delimiter] = 0:4-0:5 + >-< +0| (()) () + +[#3 Interior] = 0:6-0:6 + >< +0| (()) () + +[#3 Boundary L: Content] = 0:5-0:6 + >-< +0| (()) () +[#3 Boundary L: Removal] = 0:4-0:6 + >--< +0| (()) () + +[#3 Boundary R] = 0:6-0:7 + >-< +0| (()) () + +[#3 Insertion delimiter] = " " diff --git a/data/fixtures/scopes/yaml/disqualifyDelimiter.scope b/data/fixtures/scopes/yaml/disqualifyDelimiter.scope new file mode 100644 index 0000000000..8f609bb02b --- /dev/null +++ b/data/fixtures/scopes/yaml/disqualifyDelimiter.scope @@ -0,0 +1,15 @@ +foo: > +foo: >- +foo: >+ +--- +[#1 Content] = 0:5-0:6 + >-< +0| foo: > + +[#2 Content] = 1:5-1:7 + >--< +1| foo: >- + +[#3 Content] = 2:5-2:7 + >--< +2| foo: >+ diff --git a/packages/common/src/scopeSupportFacets/c.ts b/packages/common/src/scopeSupportFacets/c.ts index 35d83689b8..f8ddbcae24 100644 --- a/packages/common/src/scopeSupportFacets/c.ts +++ b/packages/common/src/scopeSupportFacets/c.ts @@ -8,6 +8,7 @@ const { supported, unsupported, notApplicable } = ScopeSupportFacetLevel; export const cScopeSupport: LanguageScopeSupportFacetMap = { ifStatement: supported, + disqualifyDelimiter: supported, "comment.line": supported, "comment.block": supported, diff --git a/packages/common/src/scopeSupportFacets/csharp.ts b/packages/common/src/scopeSupportFacets/csharp.ts index ba34dff18e..11d4fa4f2f 100644 --- a/packages/common/src/scopeSupportFacets/csharp.ts +++ b/packages/common/src/scopeSupportFacets/csharp.ts @@ -10,6 +10,7 @@ export const csharpScopeSupport: LanguageScopeSupportFacetMap = { ifStatement: supported, switchStatementSubject: supported, anonymousFunction: supported, + disqualifyDelimiter: supported, class: supported, "class.iteration.document": supported, diff --git a/packages/common/src/scopeSupportFacets/css.ts b/packages/common/src/scopeSupportFacets/css.ts index 20d4621843..6c95a254df 100644 --- a/packages/common/src/scopeSupportFacets/css.ts +++ b/packages/common/src/scopeSupportFacets/css.ts @@ -11,6 +11,7 @@ export const cssScopeSupport: LanguageScopeSupportFacetMap = { "string.singleLine": supported, "name.iteration.block": supported, "name.iteration.document": supported, + disqualifyDelimiter: supported, "comment.line": unsupported, }; diff --git a/packages/common/src/scopeSupportFacets/go.ts b/packages/common/src/scopeSupportFacets/go.ts index 6f7eb2cfc9..d36539280d 100644 --- a/packages/common/src/scopeSupportFacets/go.ts +++ b/packages/common/src/scopeSupportFacets/go.ts @@ -8,6 +8,7 @@ const { supported, unsupported, notApplicable } = ScopeSupportFacetLevel; export const goScopeSupport: LanguageScopeSupportFacetMap = { "comment.line": supported, + disqualifyDelimiter: supported, "textFragment.string.singleLine": supported, "textFragment.string.multiLine": supported, diff --git a/packages/common/src/scopeSupportFacets/java.ts b/packages/common/src/scopeSupportFacets/java.ts index b88b124e0f..db0ba21a8b 100644 --- a/packages/common/src/scopeSupportFacets/java.ts +++ b/packages/common/src/scopeSupportFacets/java.ts @@ -6,6 +6,8 @@ import { const { supported, notApplicable } = ScopeSupportFacetLevel; export const javaScopeSupport: LanguageScopeSupportFacetMap = { + disqualifyDelimiter: supported, + "name.foreach": supported, "value.foreach": supported, diff --git a/packages/common/src/scopeSupportFacets/javascript.ts b/packages/common/src/scopeSupportFacets/javascript.ts index b6f4398f3c..545ee53b9d 100644 --- a/packages/common/src/scopeSupportFacets/javascript.ts +++ b/packages/common/src/scopeSupportFacets/javascript.ts @@ -13,6 +13,7 @@ export const javascriptCoreScopeSupport: LanguageScopeSupportFacetMap = { regularExpression: supported, switchStatementSubject: supported, fieldAccess: supported, + disqualifyDelimiter: supported, statement: supported, "statement.iteration.document": supported, diff --git a/packages/common/src/scopeSupportFacets/latex.ts b/packages/common/src/scopeSupportFacets/latex.ts index 5e8aab29f8..63777c60b0 100644 --- a/packages/common/src/scopeSupportFacets/latex.ts +++ b/packages/common/src/scopeSupportFacets/latex.ts @@ -14,4 +14,5 @@ export const latexScopeSupport: LanguageScopeSupportFacetMap = { endTag: supported, tags: supported, environment: supported, + disqualifyDelimiter: supported, }; diff --git a/packages/common/src/scopeSupportFacets/lua.ts b/packages/common/src/scopeSupportFacets/lua.ts index 4b0ca62e2c..8d9271d901 100644 --- a/packages/common/src/scopeSupportFacets/lua.ts +++ b/packages/common/src/scopeSupportFacets/lua.ts @@ -16,4 +16,5 @@ export const luaScopeSupport: LanguageScopeSupportFacetMap = { map: supported, "branch.if": supported, namedFunction: supported, + disqualifyDelimiter: supported, }; diff --git a/packages/common/src/scopeSupportFacets/php.ts b/packages/common/src/scopeSupportFacets/php.ts index 36c3a0e5e9..405b938fc0 100644 --- a/packages/common/src/scopeSupportFacets/php.ts +++ b/packages/common/src/scopeSupportFacets/php.ts @@ -10,4 +10,5 @@ export const phpScopeSupport: LanguageScopeSupportFacetMap = { "comment.line": supported, "comment.block": supported, "textFragment.string.singleLine": supported, + disqualifyDelimiter: supported, }; diff --git a/packages/common/src/scopeSupportFacets/python.ts b/packages/common/src/scopeSupportFacets/python.ts index ec1562187e..8c9d934d90 100644 --- a/packages/common/src/scopeSupportFacets/python.ts +++ b/packages/common/src/scopeSupportFacets/python.ts @@ -15,6 +15,7 @@ export const pythonScopeSupport: LanguageScopeSupportFacetMap = { "value.resource.iteration": supported, namedFunction: supported, anonymousFunction: supported, + disqualifyDelimiter: supported, "argument.actual": supported, "argument.actual.iteration": supported, diff --git a/packages/common/src/scopeSupportFacets/ruby.ts b/packages/common/src/scopeSupportFacets/ruby.ts index 195912f65d..581e7486c0 100644 --- a/packages/common/src/scopeSupportFacets/ruby.ts +++ b/packages/common/src/scopeSupportFacets/ruby.ts @@ -9,4 +9,5 @@ const { supported, unsupported, notApplicable } = ScopeSupportFacetLevel; export const rubyScopeSupport: LanguageScopeSupportFacetMap = { "comment.line": supported, "comment.block": supported, + disqualifyDelimiter: supported, }; diff --git a/packages/common/src/scopeSupportFacets/rust.ts b/packages/common/src/scopeSupportFacets/rust.ts index fd56ae6d75..d7fc91ed8a 100644 --- a/packages/common/src/scopeSupportFacets/rust.ts +++ b/packages/common/src/scopeSupportFacets/rust.ts @@ -8,4 +8,5 @@ const { supported, unsupported, notApplicable } = ScopeSupportFacetLevel; export const rustScopeSupport: LanguageScopeSupportFacetMap = { ifStatement: supported, + disqualifyDelimiter: supported, }; diff --git a/packages/common/src/scopeSupportFacets/scala.ts b/packages/common/src/scopeSupportFacets/scala.ts index 086029888c..916d5bb3d1 100644 --- a/packages/common/src/scopeSupportFacets/scala.ts +++ b/packages/common/src/scopeSupportFacets/scala.ts @@ -8,4 +8,5 @@ const { supported, unsupported, notApplicable } = ScopeSupportFacetLevel; export const scalaScopeSupport: LanguageScopeSupportFacetMap = { ifStatement: supported, + disqualifyDelimiter: supported, }; diff --git a/packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts b/packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts index f0ab4eab8e..13581598e3 100644 --- a/packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts +++ b/packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts @@ -288,6 +288,12 @@ export const scopeSupportFacetInfos: Record< scopeType: "textFragment", }, + disqualifyDelimiter: { + description: + "Used to disqualify a token from being treated as a surrounding pair delimiter. This will usually be operators containing `>` or `<`, eg `<`, `<=`, `->`, etc", + scopeType: "disqualifyDelimiter", + }, + "branch.if": { description: "An if/elif/else branch", scopeType: "branch", diff --git a/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts index 22276f1eff..f9cd0d5a4f 100644 --- a/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts +++ b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts @@ -1,4 +1,7 @@ -import { SimpleScopeTypeType } from "../types/command/PartialTargetDescriptor.types"; +import { + SimpleScopeTypeType, + type ScopeType, +} from "../types/command/PartialTargetDescriptor.types"; const scopeSupportFacets = [ "command", @@ -76,6 +79,8 @@ const scopeSupportFacets = [ "textFragment.string.singleLine", "textFragment.string.multiLine", + "disqualifyDelimiter", + "branch.if", "branch.if.iteration", "branch.try", @@ -178,14 +183,16 @@ const textualScopeSupportFacets = [ "paragraph", "document", "nonWhitespaceSequence", + "url", + "surroundingPair", + "surroundingPair.iteration", // FIXME: Still in legacy // "boundedNonWhitespaceSequence", - "url", ] as const; export interface ScopeSupportFacetInfo { readonly description: string; - readonly scopeType: SimpleScopeTypeType; + readonly scopeType: SimpleScopeTypeType | ScopeType; readonly isIteration?: boolean; } diff --git a/packages/common/src/scopeSupportFacets/scss.ts b/packages/common/src/scopeSupportFacets/scss.ts index ee8c634e9c..457aec6518 100644 --- a/packages/common/src/scopeSupportFacets/scss.ts +++ b/packages/common/src/scopeSupportFacets/scss.ts @@ -15,4 +15,5 @@ export const scssScopeSupport: LanguageScopeSupportFacetMap = { "functionName.iteration": supported, "functionName.iteration.document": supported, "comment.line": supported, + disqualifyDelimiter: supported, }; diff --git a/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts b/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts index f4b34d5a33..4b79daf2c9 100644 --- a/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts +++ b/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts @@ -44,14 +44,30 @@ export const textualScopeSupportFacetInfos: Record< description: "A sequence of non-whitespace characters", scopeType: "nonWhitespaceSequence", }, + url: { + description: "A url", + scopeType: "url", + }, + surroundingPair: { + description: "A delimiter pair, such as parentheses or quotes", + scopeType: { + type: "surroundingPair", + delimiter: "any", + }, + }, + "surroundingPair.iteration": { + description: + "The iteration scope for delimiter pairs; should be the whole document", + scopeType: { + type: "surroundingPair", + delimiter: "any", + }, + isIteration: true, + }, // FIXME: Still in legacy // boundedNonWhitespaceSequence: { // description: // "A sequence of non-whitespace characters bounded by matching pair", // scopeType: "boundedNonWhitespaceSequence", // }, - url: { - description: "A url", - scopeType: "url", - }, }; diff --git a/packages/common/src/scopeSupportFacets/yaml.ts b/packages/common/src/scopeSupportFacets/yaml.ts index 728e294002..01f9be0e57 100644 --- a/packages/common/src/scopeSupportFacets/yaml.ts +++ b/packages/common/src/scopeSupportFacets/yaml.ts @@ -8,4 +8,5 @@ const { supported, unsupported, notApplicable } = ScopeSupportFacetLevel; export const yamlScopeSupport: LanguageScopeSupportFacetMap = { "comment.line": supported, + disqualifyDelimiter: supported, }; diff --git a/packages/common/src/types/command/PartialTargetDescriptor.types.ts b/packages/common/src/types/command/PartialTargetDescriptor.types.ts index 3589945b1c..d3197df7f3 100644 --- a/packages/common/src/types/command/PartialTargetDescriptor.types.ts +++ b/packages/common/src/types/command/PartialTargetDescriptor.types.ts @@ -113,11 +113,13 @@ export const simpleSurroundingPairNames = [ "doubleQuotes", "escapedDoubleQuotes", "escapedParentheses", - "escapedSquareBrackets", "escapedSingleQuotes", + "escapedSquareBrackets", "parentheses", "singleQuotes", "squareBrackets", + "tripleDoubleQuotes", + "tripleSingleQuotes", ] as const; export const complexSurroundingPairNames = [ "string", @@ -201,6 +203,7 @@ export const simpleScopeTypeTypes = [ "command", // Private scope types "textFragment", + "disqualifyDelimiter", ] as const; export function isSimpleScopeType( @@ -224,6 +227,11 @@ export type SurroundingPairDirection = "left" | "right"; export interface SurroundingPairScopeType { type: "surroundingPair"; delimiter: SurroundingPairName; + + /** + * @deprecated Not supported by next-gen surrounding pairs; we don't believe + * anyone uses this + */ forceDirection?: SurroundingPairDirection; /** diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts index f8773d62e6..b741917729 100644 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts @@ -1,13 +1,13 @@ -import { FlashStyle, Range, matchAll } from "@cursorless/common"; +import { FlashStyle, matchAll, Range } from "@cursorless/common"; import type { Snippets } from "../../core/Snippets"; -import { Offsets } from "../../processTargets/modifiers/surroundingPair/types"; import { ide } from "../../singletons/ide.singleton"; import type { Target } from "../../typings/target.types"; import { ensureSingleTarget, flashTargets } from "../../util/targetUtils"; import type { ActionReturnValue } from "../actions.types"; -import Substituter from "./Substituter"; import { constructSnippetBody } from "./constructSnippetBody"; import { editText } from "./editText"; +import type { Offsets } from "./Offsets"; +import Substituter from "./Substituter"; /** * This action can be used to automatically create a snippet from a target. Any diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/Offsets.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/Offsets.ts new file mode 100644 index 0000000000..0851b5f404 --- /dev/null +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/Offsets.ts @@ -0,0 +1,7 @@ +/** + * Offsets within a range or document + */ +export interface Offsets { + start: number; + end: number; +} diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/editText.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/editText.ts index 5b172a8464..64678273ec 100644 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/editText.ts +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/editText.ts @@ -1,5 +1,5 @@ +import type { Offsets } from "./Offsets"; import { sortBy } from "lodash-es"; -import { Offsets } from "../../processTargets/modifiers/surroundingPair/types"; /** * For each edit in {@link edits} replaces the given {@link Edit.offsets} in diff --git a/packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/surroundingPairsDelimiters.ts b/packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/surroundingPairsDelimiters.ts index 1b6fc57ce3..b04351e2f4 100644 --- a/packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/surroundingPairsDelimiters.ts +++ b/packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/surroundingPairsDelimiters.ts @@ -15,6 +15,8 @@ export const surroundingPairsDelimiters: Record< backtickQuotes: ["`", "`"], squareBrackets: ["[", "]"], singleQuotes: ["'", "'"], + tripleDoubleQuotes: ['"""', '"""'], + tripleSingleQuotes: ["'''", "'''"], whitespace: [" ", " "], any: null, diff --git a/packages/cursorless-engine/src/languages/LanguageDefinition.ts b/packages/cursorless-engine/src/languages/LanguageDefinition.ts index 09406e93a8..3ad3e495d3 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinition.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinition.ts @@ -1,14 +1,17 @@ import { + RawTreeSitterQueryProvider, ScopeType, SimpleScopeType, + SimpleScopeTypeType, + TreeSitter, matchAll, showError, type IDE, - type RawTreeSitterQueryProvider, - type TreeSitter, + type TextDocument, } from "@cursorless/common"; import { TreeSitterScopeHandler } from "../processTargets/modifiers/scopeHandlers"; import { TreeSitterQuery } from "./TreeSitterQuery"; +import type { QueryCapture } from "./TreeSitterQuery/QueryCapture"; import { validateQueryCaptures } from "./TreeSitterQuery/validateQueryCaptures"; /** @@ -75,6 +78,24 @@ export class LanguageDefinition { return new TreeSitterScopeHandler(this.query, scopeType as SimpleScopeType); } + + /** + * This is a low-level function that just returns a list of captures of the given + * capture name in the document. We use this in our surrounding pair code. + * + * @param document The document to search + * @param captureName The name of a capture to search for + * @returns A list of captures of the given capture name in the document + */ + getCaptures( + document: TextDocument, + captureName: SimpleScopeTypeType, + ): QueryCapture[] { + return this.query + .matches(document) + .map((match) => match.captures.find(({ name }) => name === captureName)) + .filter((capture) => capture != null); + } } /** diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts index f92fde3098..e8a65733fd 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts @@ -6,13 +6,19 @@ import { Point } from "web-tree-sitter"; * {@link MutableQueryCapture} to avoid using range/text and other mutable * parameters directly from the node. */ -export interface SimpleSyntaxNode { +interface SimpleSyntaxNode { readonly id: number; readonly type: string; + readonly parent: SimpleSyntaxNode | null; + readonly children: Array; +} + +/** + * Add start and end position to the simple syntax node. Used by the `child-range!` predicate. + */ +interface SimpleChildSyntaxNode extends SimpleSyntaxNode { readonly startPosition: Point; readonly endPosition: Point; - readonly parent: SimpleSyntaxNode | null; - readonly children: Array; } /** @@ -38,6 +44,9 @@ export interface QueryCapture { /** The insertion delimiter to use if any */ readonly insertionDelimiter: string | undefined; + + /** Returns true if this node or any of its ancestors has errors */ + hasError(): boolean; } /** @@ -58,7 +67,7 @@ export interface MutableQueryCapture extends QueryCapture { /** * The tree-sitter node that was captured. */ - readonly node: Omit; + readonly node: SimpleSyntaxNode; readonly document: TextDocument; range: Range; diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts index f2fd931d27..6184f73fa1 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts @@ -4,12 +4,13 @@ import { showError, type TreeSitter, } from "@cursorless/common"; +import { groupBy, uniq } from "lodash-es"; import { Point, Query } from "web-tree-sitter"; import { ide } from "../../singletons/ide.singleton"; -import { groupBy, uniq } from "lodash-es"; import { getNodeRange } from "../../util/nodeSelectors"; import { MutableQueryMatch, QueryCapture, QueryMatch } from "./QueryCapture"; import { checkCaptureStartEnd } from "./checkCaptureStartEnd"; +import { isContainedInErrorNode } from "./isContainedInErrorNode"; import { parsePredicates } from "./parsePredicates"; import { predicateToString } from "./predicateToString"; import { rewriteStartOfEndOf } from "./rewriteStartOfEndOf"; @@ -85,6 +86,7 @@ export class TreeSitterQuery { range: getNodeRange(node), insertionDelimiter: undefined, allowMultiple: false, + hasError: () => isContainedInErrorNode(node), })), }), ) @@ -120,6 +122,7 @@ export class TreeSitterQuery { insertionDelimiter: captures.find( (capture) => capture.insertionDelimiter != null, )?.insertionDelimiter, + hasError: () => captures.some((capture) => capture.hasError()), }; }); diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts index 3d4afa5729..8285a90c53 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts @@ -5,7 +5,10 @@ import assert from "assert"; interface TestCase { name: string; - captures: Omit[]; + captures: Omit< + QueryCapture, + "allowMultiple" | "insertionDelimiter" | "hasError" + >[]; isValid: boolean; expectedErrorMessageIds: string[]; } @@ -193,6 +196,7 @@ suite("checkCaptureStartEnd", () => { ...capture, allowMultiple: false, insertionDelimiter: undefined, + hasError: () => false, })), messages, ); diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/isContainedInErrorNode.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/isContainedInErrorNode.ts new file mode 100644 index 0000000000..8b3c46a9bc --- /dev/null +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/isContainedInErrorNode.ts @@ -0,0 +1,19 @@ +import type { SyntaxNode } from "web-tree-sitter"; + +/** + * Determines whether the given node or one of its ancestors is an error node + * @param node The node to check + * @returns True if the given node is contained in an error node + */ +export function isContainedInErrorNode(node: SyntaxNode) { + let currentNode: SyntaxNode | null = node; + + while (currentNode != null) { + if (currentNode.hasError) { + return true; + } + currentNode = currentNode.parent; + } + + return false; +} diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts index cc8e99c789..e8f87277d9 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts @@ -50,6 +50,8 @@ const testCases: TestCase[] = [ }, ]; +const hasError = () => false; + function fillOutCapture(capture: NameRange): MutableQueryCapture { return { ...capture, @@ -57,6 +59,7 @@ function fillOutCapture(capture: NameRange): MutableQueryCapture { insertionDelimiter: undefined, document: null as unknown as TextDocument, node: null as unknown as SyntaxNode, + hasError, }; } diff --git a/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts index 064b15ec10..5d77749b0a 100644 --- a/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts @@ -2,7 +2,6 @@ import { ContainingScopeModifier, EveryScopeModifier, Modifier, - SurroundingPairModifier, } from "@cursorless/common"; import { StoredTargetMap } from "../core/StoredTargets"; import { LanguageDefinitions } from "../languages/LanguageDefinitions"; @@ -29,7 +28,6 @@ import { EndOfStage, StartOfStage } from "./modifiers/PositionStage"; import { RangeModifierStage } from "./modifiers/RangeModifierStage"; import { RawSelectionStage } from "./modifiers/RawSelectionStage"; import { RelativeScopeStage } from "./modifiers/RelativeScopeStage"; -import { SurroundingPairStage } from "./modifiers/SurroundingPairStage"; import { VisibleStage } from "./modifiers/VisibleStage"; import { ScopeHandlerFactory } from "./modifiers/scopeHandlers/ScopeHandlerFactory"; import { BoundedNonWhitespaceSequenceStage } from "./modifiers/scopeTypeStages/BoundedNonWhitespaceStage"; @@ -80,19 +78,16 @@ export class ModifierStageFactoryImpl implements ModifierStageFactory { if (modifier.scopeType.type === "instance") { return new InstanceStage(this, this.storedTargets, modifier); } - return new EveryScopeStage(this, this.scopeHandlerFactory, modifier); case "ordinalScope": if (modifier.scopeType.type === "instance") { return new InstanceStage(this, this.storedTargets, modifier); } - return new OrdinalScopeStage(this, modifier); case "relativeScope": if (modifier.scopeType.type === "instance") { return new InstanceStage(this, this.storedTargets, modifier); } - return new RelativeScopeStage(this, this.scopeHandlerFactory, modifier); case "keepContentFilter": return new KeepContentFilterStage(modifier); @@ -137,12 +132,7 @@ export class ModifierStageFactoryImpl implements ModifierStageFactory { modifier, ); case "collectionItem": - return new ItemStage(this.languageDefinitions, modifier); - case "surroundingPair": - return new SurroundingPairStage( - this.languageDefinitions, - modifier as SurroundingPairModifier, - ); + return new ItemStage(this.languageDefinitions, this, modifier); default: // Default to containing syntax scope using tree sitter return new LegacyContainingSyntaxScopeStage( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts index 7f595356e1..7f03939cdf 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts @@ -9,6 +9,7 @@ import { import { LanguageDefinitions } from "../../../languages/LanguageDefinitions"; import { Target } from "../../../typings/target.types"; import { getRangeLength } from "../../../util/rangeUtils"; +import type { ModifierStageFactory } from "../../ModifierStageFactory"; import { ModifierStage } from "../../PipelineStages.types"; import { ScopeTypeTarget } from "../../targets"; import { @@ -21,6 +22,7 @@ import { tokenizeRange } from "./tokenizeRange"; export class ItemStage implements ModifierStage { constructor( private languageDefinitions: LanguageDefinitions, + private modifierStageFactory: ModifierStageFactory, private modifier: ContainingScopeModifier | EveryScopeModifier, ) {} @@ -37,17 +39,17 @@ export class ItemStage implements ModifierStage { // Then try the textual implementation if (this.modifier.type === "everyScope") { - return this.getEveryTarget(this.languageDefinitions, target); + return this.getEveryTarget(this.modifierStageFactory, target); } - return [this.getSingleTarget(this.languageDefinitions, target)]; + return [this.getSingleTarget(this.modifierStageFactory, target)]; } private getEveryTarget( - languageDefinitions: LanguageDefinitions, + modifierStageFactory: ModifierStageFactory, target: Target, ) { const itemInfos = getItemInfosForIterationScope( - languageDefinitions, + modifierStageFactory, target, ); @@ -66,11 +68,11 @@ export class ItemStage implements ModifierStage { } private getSingleTarget( - languageDefinitions: LanguageDefinitions, + modifierStageFactory: ModifierStageFactory, target: Target, ) { const itemInfos = getItemInfosForIterationScope( - languageDefinitions, + modifierStageFactory, target, ); @@ -144,10 +146,10 @@ function filterItemInfos(target: Target, itemInfos: ItemInfo[]): ItemInfo[] { } function getItemInfosForIterationScope( - languageDefinitions: LanguageDefinitions, + modifierStageFactory: ModifierStageFactory, target: Target, ) { - const { range, boundary } = getIterationScope(languageDefinitions, target); + const { range, boundary } = getIterationScope(modifierStageFactory, target); return getItemsInRange(target.editor, range, boundary); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts index 503ba34bd1..4a8c0a71fa 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -1,9 +1,13 @@ -import { Range, TextEditor, TextLine } from "@cursorless/common"; -import { LanguageDefinitions } from "../../../languages/LanguageDefinitions"; +import { + Range, + TextEditor, + TextLine, + type SurroundingPairScopeType, +} from "@cursorless/common"; import { Target } from "../../../typings/target.types"; -import { PlainTarget, SurroundingPairTarget } from "../../targets"; +import type { ModifierStageFactory } from "../../ModifierStageFactory"; +import { PlainTarget } from "../../targets"; import { fitRangeToLineContent } from "../scopeHandlers"; -import { processSurroundingPair } from "../surroundingPair"; /** * Get the iteration scope range for item scope. @@ -13,28 +17,31 @@ import { processSurroundingPair } from "../surroundingPair"; * @returns The stage iteration scope and optional surrounding pair boundaries */ export function getIterationScope( - languageDefinitions: LanguageDefinitions, + modifierStageFactory: ModifierStageFactory, target: Target, ): { range: Range; boundary?: [Range, Range] } { - let surroundingTarget = getSurroundingPair(languageDefinitions, target); + let surroundingTarget = getBoundarySurroundingPair( + modifierStageFactory, + target, + ); // Iteration is necessary in case of in valid surrounding targets (nested strings, content range adjacent to delimiter) while (surroundingTarget != null) { if ( useInteriorOfSurroundingTarget( - languageDefinitions, + modifierStageFactory, target, surroundingTarget, ) ) { return { - range: surroundingTarget.getInterior()[0].contentRange, + range: surroundingTarget.getInterior()![0].contentRange, boundary: getBoundary(surroundingTarget), }; } surroundingTarget = getParentSurroundingPair( - languageDefinitions, + modifierStageFactory, target.editor, surroundingTarget, ); @@ -47,9 +54,9 @@ export function getIterationScope( } function useInteriorOfSurroundingTarget( - languageDefinitions: LanguageDefinitions, + modifierStageFactory: ModifierStageFactory, target: Target, - surroundingTarget: SurroundingPairTarget, + surroundingTarget: Target, ): boolean { const { contentRange } = target; @@ -91,7 +98,7 @@ function useInteriorOfSurroundingTarget( // We don't look for items inside strings. // A non-string surrounding pair that is inside a surrounding string is fine. const surroundingStringTarget = getStringSurroundingPair( - languageDefinitions, + modifierStageFactory, surroundingTarget, ); if ( @@ -106,8 +113,8 @@ function useInteriorOfSurroundingTarget( return true; } -function getBoundary(surroundingTarget: SurroundingPairTarget): [Range, Range] { - return surroundingTarget.getBoundary().map((t) => t.contentRange) as [ +function getBoundary(surroundingTarget: Target): [Range, Range] { + return surroundingTarget.getBoundary()!.map((t) => t.contentRange) as [ Range, Range, ]; @@ -125,19 +132,19 @@ function characterIsWhitespaceOrMissing( } function getParentSurroundingPair( - languageDefinitions: LanguageDefinitions, + modifierStageFactory: ModifierStageFactory, editor: TextEditor, - target: SurroundingPairTarget, + target: Target, ) { const startOffset = editor.document.offsetAt(target.contentRange.start); // Can't have a parent; already at start of document if (startOffset === 0) { - return null; + return undefined; } // Step out of this pair and see if we have a parent const position = editor.document.positionAt(startOffset - 1); - return getSurroundingPair( - languageDefinitions, + return getBoundarySurroundingPair( + modifierStageFactory, new PlainTarget({ editor, contentRange: new Range(position, position), @@ -146,11 +153,11 @@ function getParentSurroundingPair( ); } -function getSurroundingPair( - languageDefinitions: LanguageDefinitions, +function getBoundarySurroundingPair( + modifierStageFactory: ModifierStageFactory, target: Target, -) { - return processSurroundingPair(languageDefinitions, target, { +): Target | undefined { + return getSurroundingPair(modifierStageFactory, target, { type: "surroundingPair", delimiter: "collectionBoundary", requireStrongContainment: true, @@ -158,12 +165,34 @@ function getSurroundingPair( } function getStringSurroundingPair( - languageDefinitions: LanguageDefinitions, + modifierStageFactory: ModifierStageFactory, target: Target, -) { - return processSurroundingPair(languageDefinitions, target, { +): Target | undefined { + return getSurroundingPair(modifierStageFactory, target, { type: "surroundingPair", delimiter: "string", requireStrongContainment: true, }); } + +function getSurroundingPair( + modifierStageFactory: ModifierStageFactory, + target: Target, + scopeType: SurroundingPairScopeType, +): Target | undefined { + const pairStage = modifierStageFactory.create({ + type: "containingScope", + scopeType, + }); + const targets = (() => { + try { + return pairStage.run(target); + } catch (_error) { + return []; + } + })(); + if (targets.length > 1) { + throw Error("Expected only one surrounding pair target"); + } + return targets[0]; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/SurroundingPairStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/SurroundingPairStage.ts deleted file mode 100644 index acf6e4cef8..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/SurroundingPairStage.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { - ContainingSurroundingPairModifier, - SurroundingPairModifier, -} from "@cursorless/common"; -import { LanguageDefinitions } from "../../languages/LanguageDefinitions"; -import type { Target } from "../../typings/target.types"; -import type { ModifierStage } from "../PipelineStages.types"; -import { SurroundingPairTarget } from "../targets"; -import { processSurroundingPair } from "./surroundingPair"; - -/** - * Applies the surrounding pair modifier to the given selection. First looks to - * see if the target is itself adjacent to or contained by a modifier token. If - * so it will expand the selection to the opposite delimiter token. Otherwise, - * or if the opposite token wasn't found, it will proceed by finding the - * smallest pair of delimiters which contains the selection. - * - * @param context Context to be leveraged by modifier - * @param selection The selection to process - * @param modifier The surrounding pair modifier information - * @returns The new selection expanded to the containing surrounding pair or - * `null` if none was found - */ -export class SurroundingPairStage implements ModifierStage { - constructor( - private languageDefinitions: LanguageDefinitions, - private modifier: SurroundingPairModifier, - ) {} - - run(target: Target): SurroundingPairTarget[] { - if (this.modifier.type === "everyScope") { - throw Error(`Unsupported every scope ${this.modifier.scopeType.type}`); - } - - return processedSurroundingPairTarget( - this.languageDefinitions, - this.modifier, - target, - ); - } -} - -function processedSurroundingPairTarget( - languageDefinitions: LanguageDefinitions, - modifier: ContainingSurroundingPairModifier, - target: Target, -): SurroundingPairTarget[] { - const outputTarget = processSurroundingPair( - languageDefinitions, - target, - modifier.scopeType, - ); - - if (outputTarget == null) { - throw new Error("Couldn't find containing pair"); - } - - return [outputTarget]; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts index bbbda031c3..582c45375f 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts @@ -51,7 +51,7 @@ export function advanceIteratorsUntil( let { value } = iteratorInfo; let done: boolean | undefined = false; - while (!criterion(value) && !done) { + while (!done && !criterion(value)) { ({ value, done } = iterator.next()); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts index 25c3575553..4150138c9a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts @@ -14,6 +14,7 @@ import { } from "./RegexScopeHandler"; import { ScopeHandlerFactory } from "./ScopeHandlerFactory"; import { SentenceScopeHandler } from "./SentenceScopeHandler/SentenceScopeHandler"; +import { SurroundingPairScopeHandler } from "./SurroundingPairScopeHandler"; import { TokenScopeHandler } from "./TokenScopeHandler"; import { WordScopeHandler } from "./WordScopeHandler/WordScopeHandler"; import type { CustomScopeType, ScopeHandler } from "./scopeHandler.types"; @@ -75,6 +76,12 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { return new CustomRegexScopeHandler(this, scopeType, languageId); case "glyph": return new GlyphScopeHandler(this, scopeType, languageId); + case "surroundingPair": + return new SurroundingPairScopeHandler( + this.languageDefinitions, + scopeType, + languageId, + ); case "custom": return scopeType.scopeHandler; case "instance": diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts deleted file mode 100644 index b51a6389a1..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Position, TextEditor } from "@cursorless/common"; -import { Direction, SurroundingPairScopeType } from "@cursorless/common"; -import { BaseScopeHandler } from "./BaseScopeHandler"; -import { TargetScope } from "./scope.types"; -import { ScopeIteratorRequirements } from "./scopeHandler.types"; - -export class SurroundingPairScopeHandler extends BaseScopeHandler { - public readonly iterationScopeType; - - protected isHierarchical = true; - - constructor( - public readonly scopeType: SurroundingPairScopeType, - _languageId: string, - ) { - super(); - this.iterationScopeType = this.scopeType; - } - - generateScopeCandidates( - _editor: TextEditor, - _position: Position, - _direction: Direction, - _hints: ScopeIteratorRequirements, - ): Iterable { - throw new Error("Method not implemented."); - } -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/SurroundingPairScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/SurroundingPairScopeHandler.ts new file mode 100644 index 0000000000..55308923c3 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/SurroundingPairScopeHandler.ts @@ -0,0 +1,109 @@ +import { + Direction, + Position, + SurroundingPairScopeType, + TextEditor, + showError, + type ScopeType, +} from "@cursorless/common"; +import type { LanguageDefinitions } from "../../../../languages/LanguageDefinitions"; +import { ide } from "../../../../singletons/ide.singleton"; +import { BaseScopeHandler } from "../BaseScopeHandler"; +import { compareTargetScopes } from "../compareTargetScopes"; +import { TargetScope } from "../scope.types"; +import { ScopeIteratorRequirements } from "../scopeHandler.types"; +import { createTargetScope } from "./createTargetScope"; +import { getDelimiterOccurrences } from "./getDelimiterOccurrences"; +import { getIndividualDelimiters } from "./getIndividualDelimiters"; +import { getSurroundingPairOccurrences } from "./getSurroundingPairOccurrences"; +import { SurroundingPairOccurrence } from "./types"; + +export class SurroundingPairScopeHandler extends BaseScopeHandler { + public readonly iterationScopeType: ScopeType = { type: "line" }; + protected isHierarchical = true; + + constructor( + private languageDefinitions: LanguageDefinitions, + public readonly scopeType: SurroundingPairScopeType, + private languageId: string, + ) { + super(); + } + + *generateScopeCandidates( + editor: TextEditor, + position: Position, + direction: Direction, + hints: ScopeIteratorRequirements, + ): Iterable { + if (this.scopeType.forceDirection != null) { + // DEPRECATED @ 2024-07-01 + void showError( + ide().messages, + "deprecatedForceDirection", + "forceDirection is deprecated. If this is important to you please file an issue on the cursorless repo.", + ); + return; + } + + const delimiterOccurrences = getDelimiterOccurrences( + this.languageDefinitions.get(this.languageId), + editor.document, + getIndividualDelimiters(this.scopeType.delimiter, this.languageId), + ); + + let surroundingPairs = getSurroundingPairOccurrences(delimiterOccurrences); + + surroundingPairs = maybeApplyEmptyTargetHack( + direction, + hints, + position, + surroundingPairs, + ); + + yield* surroundingPairs + .map((pair) => + createTargetScope( + editor, + pair, + this.scopeType.requireStrongContainment ?? false, + ), + ) + .sort((a, b) => compareTargetScopes(direction, position, a, b)); + } +} + +/** + * Applies the empty target hack, if appropriate. We are trying to detect that + * we are in a situation where the target is empty and the user has asked for + * containing scope. We use this so that in the case of `(()|)`, "take pair" + * yields the bigger pair, to be consistent with the way VSCode highlights the + * pair adjacent to your cursor. + * + * FIXME: This is a hack. We're basically using a heuristic to guess that we're + * being called from containing scope stage with empty target, but we really + * can't assume this. + */ +function maybeApplyEmptyTargetHack( + direction: Direction, + hints: ScopeIteratorRequirements, + position: Position, + surroundingPairs: SurroundingPairOccurrence[], +): SurroundingPairOccurrence[] { + if ( + direction === "forward" && + hints.containment === "required" && + hints.allowAdjacentScopes && + hints.skipAncestorScopes + ) { + return surroundingPairs.filter( + (pair, i) => + !( + pair.closingDelimiterRange.end.isEqual(position) && + surroundingPairs[i + 1]?.closingDelimiterRange.start.isEqual(position) + ), + ); + } + + return surroundingPairs; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/createTargetScope.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/createTargetScope.ts new file mode 100644 index 0000000000..37a9e1569d --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/createTargetScope.ts @@ -0,0 +1,33 @@ +import { Range, TextEditor } from "@cursorless/common"; +import { SurroundingPairTarget } from "../../../targets"; +import { TargetScope } from "../scope.types"; +import type { SurroundingPairOccurrence } from "./types"; + +/** + * Creates a target scope from a surrounding pair occurrence + */ +export function createTargetScope( + editor: TextEditor, + { openingDelimiterRange, closingDelimiterRange }: SurroundingPairOccurrence, + requireStrongContainment: boolean, +): TargetScope { + const fullRange = openingDelimiterRange.union(closingDelimiterRange); + const interiorRange = new Range( + openingDelimiterRange.end, + closingDelimiterRange.start, + ); + + return { + editor, + domain: requireStrongContainment ? interiorRange : fullRange, + getTargets: (isReversed) => [ + new SurroundingPairTarget({ + editor, + isReversed, + contentRange: fullRange, + interiorRange, + boundary: [openingDelimiterRange, closingDelimiterRange], + }), + ], + }; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/delimiterMaps.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts similarity index 50% rename from packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/delimiterMaps.ts rename to packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts index 1412215d78..4b523b2dc0 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/delimiterMaps.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts @@ -6,9 +6,31 @@ import { type IndividualDelimiterText = string | string[]; +interface Options { + /** + * If true, then the delimiter pair can only be on a single line. We use this + * flag to save us searching the entire document when we're trying to + * determine whether an ambiguous delimiter is opening or closing. The most + * salient example is strings. + */ + isSingleLine?: boolean; + + /** + * This field can be used to force us to treat the side of the delimiter as + * unknown. We usually infer this from the fact that the opening and closing + * delimiters are the same, but in some cases they are different, but the side + * is actually still unknown. In particular, this is the case for Python + * string prefixes, where if we see the prefix it doesn't necessarily mean + * that it's an opening delimiter. For example, in `" r"`, note that the `r` + * is just part of the string, not a prefix of the opening delimiter. + */ + isUnknownSide?: boolean; +} + type DelimiterMap = Record< SimpleSurroundingPairName, - [IndividualDelimiterText, IndividualDelimiterText] + | [IndividualDelimiterText, IndividualDelimiterText] + | [IndividualDelimiterText, IndividualDelimiterText, Options] >; const delimiterToText: DelimiterMap = Object.freeze({ @@ -18,16 +40,50 @@ const delimiterToText: DelimiterMap = Object.freeze({ ], backtickQuotes: ["`", "`"], curlyBrackets: [["{", "${"], "}"], - doubleQuotes: ['"', '"'], - escapedDoubleQuotes: ['\\"', '\\"'], + tripleDoubleQuotes: [[], []], + tripleSingleQuotes: [[], []], + doubleQuotes: ['"', '"', { isSingleLine: true }], + escapedDoubleQuotes: ['\\"', '\\"', { isSingleLine: true }], escapedParentheses: ["\\(", "\\)"], escapedSquareBrackets: ["\\[", "\\]"], - escapedSingleQuotes: ["\\'", "\\'"], + escapedSingleQuotes: ["\\'", "\\'", { isSingleLine: true }], parentheses: [["(", "$("], ")"], - singleQuotes: ["'", "'"], + singleQuotes: ["'", "'", { isSingleLine: true }], squareBrackets: ["[", "]"], }); +// https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals +const pythonPrefixes = [ + // Base case without a prefix + "", + // string prefixes + "r", + "u", + "R", + "U", + "f", + "F", + "fr", + "Fr", + "fR", + "FR", + "rf", + "rF", + "Rf", + "RF", + // byte prefixes + "b", + "B", + "br", + "Br", + "bR", + "BR", + "rb", + "rB", + "Rb", + "RB", +]; + // FIXME: Probably remove these as part of // https://github.com/cursorless-dev/cursorless/issues/1812#issuecomment-1691493746 const delimiterToTextOverrides: Record> = { @@ -46,11 +102,30 @@ const delimiterToTextOverrides: Record> = { }, python: { - // FIXME: We technically can't distinguish between single and double quotes - // now, but we'll revisit all this; see - // https://github.com/cursorless-dev/cursorless/issues/1812#issuecomment-1691493746 - singleQuotes: ["string_start", "string_end"], - doubleQuotes: ["string_start", "string_end"], + singleQuotes: [ + pythonPrefixes.map((prefix) => `${prefix}'`), + "'", + { isSingleLine: true, isUnknownSide: true }, + ], + doubleQuotes: [ + pythonPrefixes.map((prefix) => `${prefix}"`), + '"', + { isSingleLine: true, isUnknownSide: true }, + ], + tripleSingleQuotes: [ + pythonPrefixes.map((prefix) => `${prefix}'''`), + "'''", + { isUnknownSide: true }, + ], + tripleDoubleQuotes: [ + pythonPrefixes.map((prefix) => `${prefix}"""`), + '"""', + { isUnknownSide: true }, + ], + }, + + ruby: { + tripleDoubleQuotes: ["%Q(", ")"], }, }; @@ -67,7 +142,13 @@ export const complexDelimiterMap: Record< SimpleSurroundingPairName[] > = { any: unsafeKeys(delimiterToText), - string: ["singleQuotes", "doubleQuotes", "backtickQuotes"], + string: [ + "tripleDoubleQuotes", + "tripleSingleQuotes", + "doubleQuotes", + "singleQuotes", + "backtickQuotes", + ], collectionBoundary: [ "parentheses", "squareBrackets", @@ -96,7 +177,8 @@ export function getSimpleDelimiterMap( languageId: string | undefined, ): Record< SimpleSurroundingPairName, - [IndividualDelimiterText, IndividualDelimiterText] + | [IndividualDelimiterText, IndividualDelimiterText] + | [IndividualDelimiterText, IndividualDelimiterText, Options] > { if (languageId != null && languageId in delimiterToTextOverrides) { return { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts new file mode 100644 index 0000000000..18f42e3845 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts @@ -0,0 +1,61 @@ +import { matchAll, Range, type TextDocument } from "@cursorless/common"; +import type { LanguageDefinition } from "../../../../languages/LanguageDefinition"; +import { getDelimiterRegex } from "./getDelimiterRegex"; +import type { DelimiterOccurrence, IndividualDelimiter } from "./types"; + +/** + * Finds all occurrences of delimiters of a particular kind in a document. + * + * @param languageDefinition The language definition for the document + * @param document The document + * @param individualDelimiters A list of individual delimiters to search for + * @returns A list of occurrences of the delimiters + */ +export function getDelimiterOccurrences( + languageDefinition: LanguageDefinition | undefined, + document: TextDocument, + individualDelimiters: IndividualDelimiter[], +): DelimiterOccurrence[] { + if (individualDelimiters.length === 0) { + return []; + } + + const delimiterRegex = getDelimiterRegex(individualDelimiters); + + const disqualifyDelimiters = + languageDefinition?.getCaptures(document, "disqualifyDelimiter") ?? []; + const textFragments = + languageDefinition?.getCaptures(document, "textFragment") ?? []; + + const delimiterTextToDelimiterInfoMap = Object.fromEntries( + individualDelimiters.map((individualDelimiter) => [ + individualDelimiter.text, + individualDelimiter, + ]), + ); + + const text = document.getText(); + + return matchAll(text, delimiterRegex, (match): DelimiterOccurrence => { + const text = match[0]; + const range = new Range( + document.positionAt(match.index!), + document.positionAt(match.index! + text.length), + ); + + const isDisqualified = disqualifyDelimiters.some( + (c) => c.range.contains(range) && !c.hasError(), + ); + + const textFragmentRange = textFragments.find((c) => + c.range.contains(range), + )?.range; + + return { + delimiterInfo: delimiterTextToDelimiterInfoMap[text], + isDisqualified, + textFragmentRange, + range, + }; + }); +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts new file mode 100644 index 0000000000..5c6e0309f5 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts @@ -0,0 +1,22 @@ +import { escapeRegExp, uniq } from "lodash-es"; +import type { IndividualDelimiter } from "./types"; + +/** + * Given a list of all possible left / right delimiter instances, returns a regex + * which matches any of the individual delimiters. + * + * @param individualDelimiters A list of all possible left / right delimiter instances + * @returns A regex which matches any of the individual delimiters + */ +export function getDelimiterRegex(individualDelimiters: IndividualDelimiter[]) { + // Create a regex which is a disjunction of all possible left / right + // delimiter texts + const individualDelimiterDisjunct = uniq( + individualDelimiters.map(({ text }) => text), + ) + .map(escapeRegExp) + .join("|"); + + // Then make sure that we don't allow preceding `\` + return new RegExp(`(? { - const [leftDelimiter, rightDelimiter] = delimiterToText[delimiter]; + return delimiters.flatMap((delimiterName) => { + const [leftDelimiter, rightDelimiter, options] = + delimiterToText[delimiterName]; + const { isSingleLine = false, isUnknownSide = false } = options ?? {}; // Allow for the fact that a delimiter might have multiple ways to indicate // its opening / closing @@ -36,17 +53,26 @@ export function getIndividualDelimiters( const isLeft = leftDelimiters.includes(text); const isRight = rightDelimiters.includes(text); + const side = (() => { + if (isUnknownSide) { + return "unknown"; + } + if (isLeft && !isRight) { + return "left"; + } + if (!isLeft && isRight) { + return "right"; + } + // If delimiter text is the same for left and right, we say its side + // is "unknown", so must be determined from context. + return "unknown"; + })(); + return { text, - // If delimiter text is the same for left and right, we say it's side - // is "unknown", so must be determined from context. - side: - isLeft && !isRight - ? "left" - : isRight && !isLeft - ? "right" - : "unknown", - delimiter, + side, + delimiterName, + isSingleLine, }; }); }); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getSurroundingPairOccurrences.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getSurroundingPairOccurrences.ts new file mode 100644 index 0000000000..1605fdfdb3 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getSurroundingPairOccurrences.ts @@ -0,0 +1,88 @@ +import { DefaultMap, SimpleSurroundingPairName } from "@cursorless/common"; +import type { DelimiterOccurrence, SurroundingPairOccurrence } from "./types"; + +/** + * Given a list of occurrences of delimiters, returns a list of occurrences of + * surrounding pairs by matching opening and closing delimiters. + * + * @param delimiterOccurrences A list of occurrences of delimiters + * @returns A list of occurrences of surrounding pairs + */ +export function getSurroundingPairOccurrences( + delimiterOccurrences: DelimiterOccurrence[], +): SurroundingPairOccurrence[] { + const result: SurroundingPairOccurrence[] = []; + + /** + * A map from delimiter names to occurrences of the opening delimiter + */ + const openingDelimiterOccurrences = new DefaultMap< + SimpleSurroundingPairName, + DelimiterOccurrence[] + >(() => []); + + for (const occurrence of delimiterOccurrences) { + const { + delimiterInfo: { delimiterName, side, isSingleLine }, + isDisqualified, + textFragmentRange, + range, + } = occurrence; + + if (isDisqualified) { + continue; + } + + let openingDelimiters = openingDelimiterOccurrences.get(delimiterName); + + if (isSingleLine) { + // If single line, remove all opening delimiters that are not on the same line + // as occurrence + openingDelimiters = openingDelimiters.filter( + (openingDelimiter) => + openingDelimiter.range.start.line === range.start.line, + ); + openingDelimiterOccurrences.set(delimiterName, openingDelimiters); + } + + /** + * A list of opening delimiters that are relevant to the current occurrence. + * We exclude delimiters that are not in the same text fragment range as the + * current occurrence. + */ + const relevantOpeningDelimiters = openingDelimiters.filter( + (openingDelimiter) => + (textFragmentRange == null && + openingDelimiter.textFragmentRange == null) || + (textFragmentRange != null && + openingDelimiter.textFragmentRange != null && + openingDelimiter.textFragmentRange.isRangeEqual(textFragmentRange)), + ); + + if ( + side === "left" || + (side === "unknown" && relevantOpeningDelimiters.length % 2 === 0) + ) { + openingDelimiters.push(occurrence); + } else { + const openingDelimiter = relevantOpeningDelimiters.at(-1); + + if (openingDelimiter == null) { + continue; + } + + openingDelimiters.splice( + openingDelimiters.lastIndexOf(openingDelimiter), + 1, + ); + + result.push({ + delimiterName: delimiterName, + openingDelimiterRange: openingDelimiter.range, + closingDelimiterRange: range, + }); + } + } + + return result; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/index.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/index.ts new file mode 100644 index 0000000000..a1455c8970 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/index.ts @@ -0,0 +1 @@ +export * from "./SurroundingPairScopeHandler"; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/types.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/types.ts new file mode 100644 index 0000000000..59fd3443ba --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/types.ts @@ -0,0 +1,71 @@ +import { SimpleSurroundingPairName, type Range } from "@cursorless/common"; + +/** + * Used to indicate whether a particular side of the delimiter is left or right + * or if we do not know. Note that the terms "opening" and "closing" could be + * used instead of "left" and "right", respectively. + */ +export type DelimiterSide = "unknown" | "left" | "right"; + +/** + * A description of one possible side of a delimiter + */ +export interface IndividualDelimiter { + /** + * Which side of the delimiter this refers to + */ + side: DelimiterSide; + + /** + * Which delimiter this represents + */ + delimiterName: SimpleSurroundingPairName; + + /** + * Whether the delimiter can only appear as part of a pair that is on a single + * line + */ + isSingleLine: boolean; + + /** + * The text that can be used to represent this side of the delimiter, eg "(" + */ + text: string; +} + +/** + * A occurrence of a surrounding pair delimiter in the document + */ +export interface DelimiterOccurrence { + /** + * Information about the delimiter itself + */ + delimiterInfo: IndividualDelimiter; + + /** + * The range of the delimiter in the document + */ + range: Range; + + /** + * If `true`, this delimiter is disqualified from being considered as a + * surrounding pair delimiter, because it has been tagged as such based on a + * parse tree query. + */ + isDisqualified: boolean; + + /** + * If the delimiter is part of a text fragment, eg a string or comment, this + * will be the range of the text fragment. + */ + textFragmentRange?: Range; +} + +/** + * A occurrence of a surrounding pair (both delimiters) in the document + */ +export interface SurroundingPairOccurrence { + delimiterName: SimpleSurroundingPairName; + openingDelimiterRange: Range; + closingDelimiterRange: Range; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/BoundedNonWhitespaceStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/BoundedNonWhitespaceStage.ts index 9dda6ddf89..77b9818be9 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/BoundedNonWhitespaceStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/BoundedNonWhitespaceStage.ts @@ -8,7 +8,6 @@ import { Target } from "../../../typings/target.types"; import { ModifierStageFactory } from "../../ModifierStageFactory"; import { ModifierStage } from "../../PipelineStages.types"; import { TokenTarget } from "../../targets"; -import { processSurroundingPair } from "../surroundingPair"; /** * Intersection of NonWhitespaceSequenceStage and a surrounding pair @@ -29,20 +28,15 @@ export class BoundedNonWhitespaceSequenceStage implements ModifierStage { }); const paintTargets = paintStage.run(target); + const pairTarget = this.getPairTarget(target); - const pairInfo = processSurroundingPair(this.languageDefinitions, target, { - type: "surroundingPair", - delimiter: "any", - requireStrongContainment: true, - }); - - if (pairInfo == null) { + if (pairTarget == null) { return paintTargets; } const targets = paintTargets.flatMap((paintTarget) => { const contentRange = paintTarget.contentRange.intersection( - pairInfo.getInterior()[0].contentRange, + pairTarget.getInterior()![0].contentRange, ); if (contentRange == null || contentRange.isEmpty) { @@ -64,4 +58,26 @@ export class BoundedNonWhitespaceSequenceStage implements ModifierStage { return targets; } + + private getPairTarget(target: Target): Target | undefined { + const pairStage = this.modifierStageFactory.create({ + type: "containingScope", + scopeType: { + type: "surroundingPair", + delimiter: "any", + requireStrongContainment: true, + }, + }); + const targets = (() => { + try { + return pairStage.run(target); + } catch (_error) { + return []; + } + })(); + if (targets.length > 1) { + throw Error("Expected only one surrounding pair target"); + } + return targets[0]; + } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/extractSelectionFromSurroundingPairOffsets.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/extractSelectionFromSurroundingPairOffsets.ts deleted file mode 100644 index a25fbf334c..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/extractSelectionFromSurroundingPairOffsets.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Range, Selection, TextDocument } from "@cursorless/common"; -import { SurroundingPairOffsets } from "./types"; - -export interface SurroundingPairInfo { - contentRange: Selection; - boundary: [Range, Range]; - interiorRange: Range; -} - -/** - * Given offsets describing a surrounding pair, returns a selection - * - * @param document The document containing the pairs - * @param baseOffset The base offset to be added to all given offsets - * @param surroundingPairOffsets A pair of start/end offsets corresponding to a delimiter pair - * @param delimiterInclusion Whether to include / exclude the delimiters themselves - * @returns A selection corresponding to the delimiter pair - */ -export function extractSelectionFromSurroundingPairOffsets( - document: TextDocument, - baseOffset: number, - surroundingPairOffsets: SurroundingPairOffsets, -): SurroundingPairInfo { - const interior = new Range( - document.positionAt(baseOffset + surroundingPairOffsets.leftDelimiter.end), - document.positionAt( - baseOffset + surroundingPairOffsets.rightDelimiter.start, - ), - ); - const boundary: [Range, Range] = [ - new Range( - document.positionAt( - baseOffset + surroundingPairOffsets.leftDelimiter.start, - ), - document.positionAt( - baseOffset + surroundingPairOffsets.leftDelimiter.end, - ), - ), - new Range( - document.positionAt( - baseOffset + surroundingPairOffsets.rightDelimiter.start, - ), - document.positionAt( - baseOffset + surroundingPairOffsets.rightDelimiter.end, - ), - ), - ]; - - return { - contentRange: new Selection( - document.positionAt( - baseOffset + surroundingPairOffsets.leftDelimiter.start, - ), - document.positionAt( - baseOffset + surroundingPairOffsets.rightDelimiter.end, - ), - ), - boundary, - interiorRange: interior, - }; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findDelimiterPairAdjacentToSelection.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findDelimiterPairAdjacentToSelection.ts deleted file mode 100644 index 47194f2bdc..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findDelimiterPairAdjacentToSelection.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { SurroundingPairScopeType } from "@cursorless/common"; -import { findOppositeDelimiter } from "./findOppositeDelimiter"; -import { getSurroundingPairOffsets } from "./getSurroundingPairOffsets"; -import { - DelimiterOccurrence, - Offsets, - PossibleDelimiterOccurrence, - SurroundingPairOffsets, -} from "./types"; -import { weaklyContains } from "./weaklyContains"; - -/** - * Looks for a surrounding pair where one of its delimiters contains the entire selection. - * - * @param initialIndex The index of the first delimiter to try within the delimiter occurrences list. Expected to be - * the index of the first delimiter whose end offset is greater than or equal to - * the end offset of the selection. - * @param delimiterOccurrences A list of delimiter occurrences. Expected to be sorted by offsets - * @param selectionOffsets The offsets of the selection - * @param bailOnUnmatchedAdjacent If `true`, immediately return null if we find - * an adjacent delimiter that we can't find a match for. This variable will - * be true if the current iteration can't see the full document. In that - * case, we'd like to fail and let a subsequent pass try again in case - * the matching delimiter is outside the range we're looking. - * @returns The offsets of a surrounding pair, one of whose delimiters is - * adjacent to or containing the selection. Returns `null` if such a pair - * can't be found in the given list of delimiter occurrences. - */ -export function findDelimiterPairAdjacentToSelection( - initialIndex: number, - delimiterOccurrences: PossibleDelimiterOccurrence[], - selectionOffsets: Offsets, - scopeType: SurroundingPairScopeType, - bailOnUnmatchedAdjacent: boolean = false, -): SurroundingPairOffsets | null { - const indicesToTry = [initialIndex + 1, initialIndex]; - - for (const index of indicesToTry) { - const delimiterOccurrence = delimiterOccurrences[index]; - - if ( - delimiterOccurrence != null && - weaklyContains(delimiterOccurrence.offsets, selectionOffsets) - ) { - const { delimiterInfo } = delimiterOccurrence; - - if (delimiterInfo != null) { - const possibleMatch = findOppositeDelimiter( - delimiterOccurrences, - index, - delimiterInfo, - scopeType.forceDirection, - ); - - if (possibleMatch != null) { - const surroundingPairOffsets = getSurroundingPairOffsets( - delimiterOccurrence as DelimiterOccurrence, - possibleMatch, - ); - - if ( - !scopeType.requireStrongContainment || - (surroundingPairOffsets.leftDelimiter.start < - selectionOffsets.start && - surroundingPairOffsets.rightDelimiter.end > selectionOffsets.end) - ) { - return surroundingPairOffsets; - } - } else if (bailOnUnmatchedAdjacent) { - return null; - } - } - } - } - - return null; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findDelimiterPairContainingSelection.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findDelimiterPairContainingSelection.ts deleted file mode 100644 index b3a1e0220d..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findDelimiterPairContainingSelection.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - SimpleSurroundingPairName, - SurroundingPairScopeType, -} from "@cursorless/common"; -import { generateUnmatchedDelimiters } from "./generateUnmatchedDelimiters"; -import { getSurroundingPairOffsets } from "./getSurroundingPairOffsets"; -import { - Offsets, - PossibleDelimiterOccurrence, - SurroundingPairOffsets, -} from "./types"; - -/** - * Looks for a surrounding pair that contains the selection, returning null if none is found. - * - * Our approach is to first initialize two generators, one scanning rightwards - * and one scanning leftwards. The generator scanning rightwards starts at the - * first delimiter whose end offset is greater than or equal to the end offset - * of the selection. The generator scanning leftwards starts at the token just - * prior to the start token for the rightward scanner. - * - * We start with the right generator, proceeding until we find any acceptable - * unmatched closing delimiter. We then advance the left generator, looking - * only for an unmatched opening delimiter that matches the closing delimiter - * we found in our rightward scan. - * - * If the delimiter found by our leftward scan is before or equal to the start - * of the selection, we return the delimiter pair. If not, we loop back and - * scan left / right again, repeating the process until our leftward or - * rightward scan runs out of delimiters. - * - * @param initialIndex The index of the first delimiter to try within the delimiter occurrences list. Expected to be - * the index of the first delimiter whose end offset is greater than or equal to - * the end offset of the selection. - * @param delimiterOccurrences A list of delimiter occurrences. Expected to be sorted by offsets - * @param acceptableDelimiters A list of names of acceptable delimiters to look for - * @param selectionOffsets The offsets of the selection - * @returns The offsets of the surrounding pair containing the selection, or - * null if none is found - */ -export function findDelimiterPairContainingSelection( - initialIndex: number, - delimiterOccurrences: PossibleDelimiterOccurrence[], - acceptableDelimiters: SimpleSurroundingPairName[], - selectionOffsets: Offsets, - scopeType: SurroundingPairScopeType, -): SurroundingPairOffsets | null { - // Accept any delimiter when scanning right - const acceptableRightDelimiters = acceptableDelimiters; - - // When scanning left, we'll populate this list with just the delimiter we - // found on our rightward pass. - let acceptableLeftDelimiters: SimpleSurroundingPairName[] = []; - - const rightDelimiterGenerator = generateUnmatchedDelimiters( - delimiterOccurrences, - initialIndex, - () => acceptableRightDelimiters, - true, - ); - - // Start just to the left of the delimiter we start from in our rightward - // pass - const leftDelimiterGenerator = generateUnmatchedDelimiters( - delimiterOccurrences, - initialIndex - 1, - () => acceptableLeftDelimiters, - false, - ); - - while (true) { - // Scan right until we find an acceptable unmatched closing delimiter - const rightNext = rightDelimiterGenerator.next(); - if (rightNext.done) { - return null; - } - const rightDelimiterOccurrence = rightNext.value!; - - // Then scan left until we find an unmatched delimiter matching the - // delimiter we found in our rightward pass. - acceptableLeftDelimiters = [ - rightDelimiterOccurrence.delimiterInfo.delimiter, - ]; - const leftNext = leftDelimiterGenerator.next(); - if (leftNext.done) { - return null; - } - const leftDelimiterOccurrence = leftNext.value!; - - // If left delimiter is left of our selection, we return it. Otherwise - // loop back and continue scanning outwards. - if (leftDelimiterOccurrence.offsets.start <= selectionOffsets.start) { - if ( - scopeType.requireStrongContainment && - !( - leftDelimiterOccurrence.offsets.end <= selectionOffsets.start && - rightDelimiterOccurrence.offsets.start >= selectionOffsets.end - ) - ) { - // If we require strong containment, continue searching for something - // bigger if the selection overlaps either delimiter - continue; - } - - return getSurroundingPairOffsets( - leftDelimiterOccurrence, - rightDelimiterOccurrence, - ); - } - } -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findOppositeDelimiter.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findOppositeDelimiter.ts deleted file mode 100644 index 6a0e8c3bb4..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findOppositeDelimiter.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { SurroundingPairDirection } from "@cursorless/common"; -import { findUnmatchedDelimiter } from "./generateUnmatchedDelimiters"; -import { - DelimiterOccurrence, - DelimiterSide, - IndividualDelimiter, - PossibleDelimiterOccurrence, -} from "./types"; - -/** - * Given a delimiter, scans in the appropriate direction for a matching - * opposite delimiter. If we don't know which direction the delimiter is facing - * (eg for a `"`), we first scan right, then left if nothing is found to the - * right. This algorithm will get confused in text files, but keep in mind - * that for languages with a parse tree, the delimiter occurrence will usually - * know which direction it is based on where it sits in the parse tree. That - * information will be reflected on the `IndividualDelimiter` itself. - * - * @param delimiterOccurrences A list of delimiter occurrences. Expected to be sorted by offsets - * @param index The index of the delimiter whose opposite we're looking for - * @param delimiterInfo The delimiter info for the delimiter occurrence at the - * given index. Just passed through for efficiency rather than having to - * look it up again. Equivalent to `delimiterOccurrences[index].delimiterInfo` - * @returns The opposite delimiter, if found; otherwise `null` - */ -export function findOppositeDelimiter( - delimiterOccurrences: PossibleDelimiterOccurrence[], - index: number, - delimiterInfo: IndividualDelimiter, - forceDirection: "left" | "right" | undefined, -): DelimiterOccurrence | null { - const { side, delimiter } = delimiterInfo; - - for (const direction of getDirections(side, forceDirection)) { - const unmatchedDelimiter = findUnmatchedDelimiter( - delimiterOccurrences, - direction === "right" ? index + 1 : index - 1, - [delimiter], - direction === "right", - ); - - if (unmatchedDelimiter != null) { - return unmatchedDelimiter; - } - } - - return null; -} - -function getDirections( - side: DelimiterSide, - forceDirection: SurroundingPairDirection | undefined, -): SurroundingPairDirection[] { - if (forceDirection != null) { - return [forceDirection]; - } - - switch (side) { - case "left": - return ["right"]; - case "right": - return ["left"]; - case "unknown": - return ["right", "left"]; - } -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts deleted file mode 100644 index 2550595cde..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { sortedIndexBy } from "lodash-es"; -import { - SimpleSurroundingPairName, - SurroundingPairScopeType, -} from "@cursorless/common"; -import { findDelimiterPairAdjacentToSelection } from "./findDelimiterPairAdjacentToSelection"; -import { findDelimiterPairContainingSelection } from "./findDelimiterPairContainingSelection"; -import { - SurroundingPairOffsets, - Offsets, - PossibleDelimiterOccurrence, -} from "./types"; - -/** - * This function implements the core high-level surrounding pair algorithm - * shared by both the parse tree and textual implementations. - * - * We first look for any delimiter pair where one of the delimiters itself - * contains our selection, for example if the user refers to a mark which is a - * delimiter token, or if the user's cursor is right next to a delimiter. - * - * If we don't find a delimiter pair that way, we instead look for the smallest - * delimiter pair that contains the selection. - * - * @param delimiterOccurrences A list of delimiter occurrences. Expected to be sorted by offsets - * @param acceptableDelimiters A list of names of acceptable delimiters to look for - * @param selectionOffsets The offsets of the selection - * @param bailOnUnmatchedAdjacent If `true`, immediately return null if we find - * an adjacent delimiter that we can't find a match for. This variable will - * be true if the current iteration can't see the full document. In that - * case, we'd like to fail and let a subsequent pass try again in case - * the matching delimiter is outside the range we're looking. - * @returns - */ -export function findSurroundingPairCore( - scopeType: SurroundingPairScopeType, - delimiterOccurrences: PossibleDelimiterOccurrence[], - acceptableDelimiters: SimpleSurroundingPairName[], - selectionOffsets: Offsets, - bailOnUnmatchedAdjacent: boolean = false, -): SurroundingPairOffsets | null { - /** - * The initial index from which to start both of our searches. We set this - * index to the index of the first delimiter whose end offset is greater than - * or equal to the end offset of the selection. - */ - const initialIndex = sortedIndexBy<{ - offsets: Offsets; - }>( - delimiterOccurrences, - { - offsets: selectionOffsets, - }, - "offsets.end", - ); - - // First look for delimiter pair where one delimiter contains the selection. - const delimiterPairAdjacentToSelection: SurroundingPairOffsets | null = - findDelimiterPairAdjacentToSelection( - initialIndex, - delimiterOccurrences, - selectionOffsets, - scopeType, - bailOnUnmatchedAdjacent, - ); - - if (delimiterPairAdjacentToSelection != null) { - return delimiterPairAdjacentToSelection; - } - - // Then look for the smallest delimiter pair containing the selection. - return findDelimiterPairContainingSelection( - initialIndex, - delimiterOccurrences, - acceptableDelimiters, - selectionOffsets, - scopeType, - ); -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts deleted file mode 100644 index 90d2511711..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { Range, TextDocument, TextEditor } from "@cursorless/common"; -import type { SyntaxNode } from "web-tree-sitter"; -import { - SimpleSurroundingPairName, - SurroundingPairScopeType, -} from "@cursorless/common"; -import { getNodeRange } from "../../../util/nodeSelectors"; -import { isContainedInErrorNode } from "../../../util/treeSitterUtils"; -import { extractSelectionFromSurroundingPairOffsets } from "./extractSelectionFromSurroundingPairOffsets"; -import { findSurroundingPairCore } from "./findSurroundingPairCore"; -import { getIndividualDelimiters } from "./getIndividualDelimiters"; -import { - IndividualDelimiter, - Offsets, - PossibleDelimiterOccurrence, -} from "./types"; - -/** - * Implements the version of the surrounding pair finding algorithm that - * leverages the parse tree. We use this algorithm when we are in a language - * for which we have parser support, unless we are in a string or comment, where - * we revert to text-based. - * - * The approach is actually roughly the same as the approach we use when we do - * not have access to a parse tree. In both cases we create a list of - * candidate delimiters in the region of the selection, and then pass them to - * the core algorithm, implemented by findSurroundingPairCore. - * - * To generate a list of delimiters to pass to findSurroundingPairCore, we repeatedly walk up the parse tree starting at the given node. Each time, we ask for all descendant tokens whose type is that of one of the delimiters that we're looking for. - * repeatedly walk up the parse tree starting at the given node. Each time, we - * ask for all descendant tokens whose type is that of one of the delimiters - * that we're looking for, and pass this list of tokens to - * findSurroundingPairCore. - * - * Note that walking up the hierarchy one parent at a time is just an - * optimization to avoid handling the entire file if we don't need to. The - * result would be the same if we just operated on the root node of the parse - * tree, just slower if our delimiter pair is actually contained in a small - * piece of a large file. - * - * The main benefits of the parse tree-based approach over the text-based - * approach are the following: - * - * - We can leverage the lexer to ensure that we only consider proper language tokens - * - We can let the language normalize surface forms of delimiter types, so eg - * in Python the leading `f"` on an f-string just has type `"` like any other - * string. - * - We can more easily narrow the scope of our search by walking up the parse tree - * - The actual lexing is done in fast wasm code rather than using a regex - * - We can disambiguate delimiters whose opening and closing symbol is the - * same (eg `"`). Without a parse tree we have to guess whether it is an - * opening or closing quote. - * - * @param editor The text editor containing the selection - * @param selection The selection to find surrounding pair around - * @param node A parse tree node overlapping with the selection - * @param delimiters The acceptable surrounding pair names - * @returns The newly expanded selection, including editor info - */ -export function findSurroundingPairParseTreeBased( - editor: TextEditor, - selection: Range, - node: SyntaxNode, - delimiters: SimpleSurroundingPairName[], - scopeType: SurroundingPairScopeType, -) { - const document: TextDocument = editor.document; - - const individualDelimiters = getIndividualDelimiters( - document.languageId, - delimiters, - ); - - const delimiterTextToDelimiterInfoMap = Object.fromEntries( - individualDelimiters.map((individualDelimiter) => [ - individualDelimiter.text, - individualDelimiter, - ]), - ); - - const selectionOffsets = { - start: document.offsetAt(selection.start), - end: document.offsetAt(selection.end), - }; - - /** - * Context to pass to nested call - */ - const context: Context = { - delimiterTextToDelimiterInfoMap, - individualDelimiters, - delimiters, - selectionOffsets, - scopeType, - }; - - // Walk up the parse tree from parent to parent until we find a node whose - // descendants contain an appropriate matching pair. - for ( - let currentNode: SyntaxNode | null = node; - currentNode != null; - currentNode = currentNode.parent - ) { - // Just bail early if the node doesn't completely contain our selection as - // it is a lost cause. - if (!getNodeRange(currentNode).contains(selection)) { - continue; - } - - // Here we apply the core algorithm - const pairOffsets = findSurroundingPairContainedInNode( - context, - currentNode, - ); - - // And then perform postprocessing - if (pairOffsets != null) { - return extractSelectionFromSurroundingPairOffsets( - document, - 0, - pairOffsets, - ); - } - } - - return null; -} - -/** - * Context to pass to nested call - */ -interface Context { - /** - * Map from raw text to info about the delimiter at that point - */ - delimiterTextToDelimiterInfoMap: { - [k: string]: IndividualDelimiter; - }; - - /** - * A list of all opening / closing delimiters that we are considering - */ - individualDelimiters: IndividualDelimiter[]; - - /** - * The names of the delimiters that we're considering - */ - delimiters: SimpleSurroundingPairName[]; - - /** - * The offsets of the selection - */ - selectionOffsets: Offsets; - - scopeType: SurroundingPairScopeType; -} - -/** - * This function is called at each node as we walk up the ancestor hierarchy - * from our start node. It finds all possible delimiters descending from the - * node and passes them to the findSurroundingPairCore algorithm. - * - * @param context Extra context to be used by this function - * @param node The current node to consider - * @returns The offsets of the matching surrounding pair, or `null` if none is found - */ -function findSurroundingPairContainedInNode( - context: Context, - node: SyntaxNode, -) { - const { - delimiterTextToDelimiterInfoMap, - individualDelimiters, - delimiters, - selectionOffsets, - scopeType, - } = context; - - /** - * A list of all delimiter nodes descending from `node`, as determined by - * their type. - * Handles the case of error nodes with no text. https://github.com/cursorless-dev/cursorless/issues/688 - */ - const possibleDelimiterNodes = node - .descendantsOfType(individualDelimiters.map(({ text }) => text)) - .filter((node) => !(node.text === "" && node.hasError)); - - /** - * A list of all delimiter occurrences, generated from the delimiter nodes. - */ - const delimiterOccurrences: PossibleDelimiterOccurrence[] = - possibleDelimiterNodes.map((delimiterNode) => { - return { - offsets: { - start: delimiterNode.startIndex, - end: delimiterNode.endIndex, - }, - get delimiterInfo() { - const delimiterInfo = - delimiterTextToDelimiterInfoMap[delimiterNode.type]; - - // Distinguish between a greater-than sign and an angle bracket by - // looking at its position within its parent node. - if ( - delimiterInfo.delimiter === "angleBrackets" && - inferDelimiterSide(delimiterNode) !== delimiterInfo.side && - !isContainedInErrorNode(delimiterNode) - ) { - return undefined; - } - - // NB: If side is `"unknown"`, ie we cannot determine whether - // something is a left or right delimiter based on its text / type - // alone (eg `"`), we assume it is a left delimiter if it is the - // first child of its parent, and right delimiter otherwise. This - // approach might not always work, but seems to work in the - // languages we've tried. - const side = - delimiterInfo.side === "unknown" && scopeType.forceDirection == null - ? inferDelimiterSide(delimiterNode) - : delimiterInfo.side; - - return { - ...delimiterInfo, - side, - }; - }, - }; - }); - - // Just run core algorithm once we have our list of delimiters. - return findSurroundingPairCore( - scopeType, - delimiterOccurrences, - delimiters, - selectionOffsets, - - // If we're not the root node of the parse tree (ie `node.parent != - // null`), we tell `findSurroundingPairCore` to bail if it finds a - // delimiter adjacent to our selection, but doesn't find its opposite - // delimiter within our list. We do so because it's possible that the - // adjacent delimiter's opposite might be found when we run again on a - // parent node later. - node.parent != null, - ); -} - -function inferDelimiterSide(delimiterNode: SyntaxNode) { - return delimiterNode.parent?.firstChild?.equals(delimiterNode) - ? "left" - : delimiterNode.parent?.lastChild?.equals(delimiterNode) - ? "right" - : ("unknown" as const); -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts deleted file mode 100644 index 9d6566dd25..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { - Range, - SimpleSurroundingPairName, - SurroundingPairName, - SurroundingPairScopeType, - TextDocument, - TextEditor, - matchAll, -} from "@cursorless/common"; -import { escapeRegExp, findLast, uniq } from "lodash-es"; -import { extractSelectionFromSurroundingPairOffsets } from "./extractSelectionFromSurroundingPairOffsets"; -import { findSurroundingPairCore } from "./findSurroundingPairCore"; -import { getIndividualDelimiters } from "./getIndividualDelimiters"; -import { - IndividualDelimiter, - Offsets, - PossibleDelimiterOccurrence, - SurroundingPairOffsets, -} from "./types"; - -/** - * The initial range length that we start by scanning - */ -const INITIAL_SCAN_LENGTH = 200; - -/** - * The maximum range we're willing to scan - */ -const MAX_SCAN_LENGTH = 50000; - -/** - * The factor by which to expand the search range at each iteration - */ -const SCAN_EXPANSION_FACTOR = 3; - -/** - * Implements the version of the surrounding pair finding algorithm that - * just looks at text. We use this algorithm when we are in a language - * for which we do not have parser support, or if we have parse tree support - * but the selection is in a string or comment. - * - * The approach is to create a list of candidate delimiters in the given range, - * and then pass them to the core algorithm, implemented by - * findSurroundingPairCore. - * - * To generate a list of delimiters to pass to findSurroundingPairCore, we - * run a regex on the entire range to find all delimiter texts, using a - * negative lookbehind to ensure they're not preceded by `\`. - * - * The main drawbacks of the text-based approach are the following: - * - * - We can get confused by delimiters whose opening and closing symbol is the - * same (eg `"`). Without a parse tree we have to guess whether it is an - * opening or closing quote. - * - We need to parse the whole range from the start because otherwise it is - * difficult to handle the case where one delimiter text is a subset of - * another, eg `"` and `\"`. We could handle this another way if performance - * becomes a bottleneck. - * - We cannot understand special features of a language, eg that `f"` is a - * form of opening quote in Python. - * - * @param editor The text editor containing the selection - * @param range The selection to find surrounding pair around - * @param allowableRange The range in which to look for delimiters, or the - * entire document if `null` - * @param delimiters The acceptable surrounding pair names - * @returns The newly expanded selection, including editor info - */ -export function findSurroundingPairTextBased( - editor: TextEditor, - range: Range, - allowableRange: Range | null, - delimiters: SimpleSurroundingPairName[], - scopeType: SurroundingPairScopeType, -) { - const document: TextDocument = editor.document; - const fullRange = allowableRange ?? document.range; - - const individualDelimiters = getIndividualDelimiters(undefined, delimiters); - - const delimiterTextToDelimiterInfoMap = Object.fromEntries( - individualDelimiters.map((individualDelimiter) => [ - individualDelimiter.text, - individualDelimiter, - ]), - ); - - /** - * Regex to use to find delimiters - */ - const delimiterRegex = getDelimiterRegex(individualDelimiters); - - /** - * The offset of the allowable range within the document. All offsets are - * taken relative to this range. - */ - const fullRangeOffsets = { - start: document.offsetAt(fullRange.start), - end: document.offsetAt(fullRange.end), - }; - const selectionOffsets = { - start: document.offsetAt(range.start), - end: document.offsetAt(range.end), - }; - - /** - * Context to pass to nested call - */ - const context: Context = { - scopeType, - delimiterRegex, - delimiters, - delimiterTextToDelimiterInfoMap, - }; - - for ( - let scanLength = INITIAL_SCAN_LENGTH; - scanLength < MAX_SCAN_LENGTH; - scanLength *= SCAN_EXPANSION_FACTOR - ) { - /** - * The current range in which to look. Here we take the full range and - * restrict it based on the current scan length - */ - const currentRangeOffsets = { - start: Math.max( - fullRangeOffsets.start, - selectionOffsets.end - scanLength / 2, - ), - end: Math.min( - fullRangeOffsets.end, - selectionOffsets.end + scanLength / 2, - ), - }; - - const currentRange = new Range( - document.positionAt(currentRangeOffsets.start), - document.positionAt(currentRangeOffsets.end), - ); - - // Just bail early if the range doesn't completely contain our selection as - // it is a lost cause. - if (!currentRange.contains(range)) { - continue; - } - - // Here we apply the core algorithm. This algorithm operates relative to the - // string that it receives so we need to adjust the selection range before - // we pass it in and then later we will adjust to the offsets that it - // returns - const adjustedSelectionOffsets = { - start: selectionOffsets.start - currentRangeOffsets.start, - end: selectionOffsets.end - currentRangeOffsets.start, - }; - - const pairOffsets = getDelimiterPairOffsets( - context, - document.getText(currentRange), - adjustedSelectionOffsets, - currentRangeOffsets.start === fullRangeOffsets.start, - currentRangeOffsets.end === fullRangeOffsets.end, - ); - - if (pairOffsets != null) { - // And then perform postprocessing - return extractSelectionFromSurroundingPairOffsets( - document, - currentRangeOffsets.start, - pairOffsets, - ); - } - - // If the current range is greater than are equal to the full range then we - // should stop expanding - if (currentRange.contains(fullRange)) { - break; - } - } - - return null; -} - -function getDelimiterRegex(individualDelimiters: IndividualDelimiter[]) { - // Create a regex which is a disjunction of all possible left / right - // delimiter texts - const individualDelimiterDisjunct = uniq( - individualDelimiters.map(({ text }) => text), - ) - .map(escapeRegExp) - .join("|"); - - // Then make sure that we don't allow preceding `\` - return new RegExp(`(? { - const startOffset = match.index!; - const matchText = match[0]; - - // NB: It is important to cache here because otherwise the algorithm that - // disambiguates delimiters of unknown side goes badly super linear - let hasCachedDelimiterInfo = false; - let cachedDelimiterInfo: IndividualDelimiter | undefined = undefined; - - return { - offsets: { - start: startOffset, - end: startOffset + matchText.length, - }, - - get delimiterInfo() { - if (hasCachedDelimiterInfo) { - return cachedDelimiterInfo; - } - - const rawDelimiterInfo = delimiterTextToDelimiterInfoMap[matchText]; - - const side = - rawDelimiterInfo.side === "unknown" && forceDirection == null - ? inferDelimiterSide( - text, - delimiterOccurrences, - index, - rawDelimiterInfo?.delimiter, - startOffset, - ) - : rawDelimiterInfo.side; - - const delimiterInfo = { ...rawDelimiterInfo, side }; - - hasCachedDelimiterInfo = true; - cachedDelimiterInfo = delimiterInfo; - - return delimiterInfo; - }, - }; - }, - ); - - // Then just run core algorithm - const surroundingPair = findSurroundingPairCore( - scopeType, - delimiterOccurrences, - delimiters, - selectionOffsets, - !isAtStartOfFullRange || !isAtEndOfFullRange, - ); - - // If we're not at the start of the full range, or we're not at the end of the - // full range then we get nervous if the delimiter we found is at the end of - // the range which is not complete, because we might have cut a token in half. - // In this case we return null and let the next iteration handle it using a - // larger range. - if ( - surroundingPair == null || - (!isAtStartOfFullRange && surroundingPair.leftDelimiter.start === 0) || - (!isAtEndOfFullRange && - surroundingPair.rightDelimiter.end === text.length - 1) - ) { - return null; - } - - return surroundingPair; -} - -/** - * Attempts to infer the side of a given delimiter of unknown side by using a - * simple heuristic. - * - * If there is a delimiter of the same type preceding the given delimiter on the - * same line then this delimiter will be of opposite side. If there is no - * delimiter proceeding this one on the same line then this delimiter will be - * considered a left delimiter. - * - * Note that this effectively ends up becoming a recursive algorithm because - * when we ask the proceeding delimiter what side it is it will use this same - * algorithm, which will then look to its left. - * - * NB: We must be careful in this algorithm not to access the delimiter info of - * the current delimiter by using the `delimiterOccurrences` list because that - * will result in infinite recursion because this function is called when we - * lazily construct the delimiter info. - * - * @param fullText The full text containing the delimiters - * @param delimiterOccurrences A list of all delimiter occurrences - * @param index The index of the current delimiter in the delimiter list - * @param delimiter The current delimiter type - * @param occurrenceStartOffset The start offset of the current delimiter within - * the full text - * @returns The inferred side of the delimiter - */ -function inferDelimiterSide( - fullText: string, - delimiterOccurrences: PossibleDelimiterOccurrence[], - index: number, - delimiter: SurroundingPairName, - occurrenceStartOffset: number, -) { - const previousOccurrence = - index === 0 - ? null - : findLast( - delimiterOccurrences, - (delimiterOccurrence) => - delimiterOccurrence.delimiterInfo?.delimiter === delimiter, - index - 1, - ); - - if ( - previousOccurrence == null || - fullText - .substring(previousOccurrence.offsets.end, occurrenceStartOffset) - .includes("\n") - ) { - return "left"; - } - - return previousOccurrence.delimiterInfo!.side === "left" ? "right" : "left"; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/generateUnmatchedDelimiters.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/generateUnmatchedDelimiters.ts deleted file mode 100644 index 88d1d3d34d..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/generateUnmatchedDelimiters.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { range } from "lodash-es"; -import { SimpleSurroundingPairName } from "@cursorless/common"; -import { - DelimiterOccurrence, - DelimiterSide, - PossibleDelimiterOccurrence, -} from "./types"; - -/** - * Finds the first instance of an unmatched delimiter in the given direction - * - * This function is a simplified version of generateUnmatchedDelimiters, so look - * there for details of the algorithm - * - * @param delimiterOccurrences A list of delimiter occurrences. Expected to be sorted by offsets - * @param initialIndex The index of the delimiter to start from - * @param acceptableDelimiters A list of names of acceptable delimiters to look - * for - * @param lookForward Whether to scan forwards or backwards - * @returns The first acceptable unmatched delimiter, if one is found otherwise null - */ -export function findUnmatchedDelimiter( - delimiterOccurrences: PossibleDelimiterOccurrence[], - initialIndex: number, - acceptableDelimiters: SimpleSurroundingPairName[], - lookForward: boolean, -): DelimiterOccurrence | null { - const generatorResult = generateUnmatchedDelimiters( - delimiterOccurrences, - initialIndex, - () => acceptableDelimiters, - lookForward, - ).next(); - - return generatorResult.done ? null : generatorResult.value; -} - -/** - * This function is the heart of our surrounding pair algorithm. It scans in - * one direction (either forwards or backwards) through a list of delimiters, - * yielding each unmatched delimiter that it finds. - * - * The algorithm proceeds by keeping a map from delimiter names to counts. Every - * time it sees an instance of an opening or closing delimiter of the given - * type, it will either increment or decrement the counter for the given - * delimiter, depending which direction we're scanning. - * - * If the count for any delimiter drops to -1, we yield it because it means it - * is unmatched. - * - * @param delimiterOccurrences A list of delimiter occurrences. Expected to be - * sorted by offsets - * @param initialIndex The index of the delimiter to start from - * @param getCurrentAcceptableDelimiters A function that returns a list of names - * of acceptable delimiters to look for. We expect that this list might change - * every time we yield, depending on the outcome of the scan in the other - * direction - * @param lookForward Whether to scan forwards or backwards - * @yields Occurrences of unmatched delimiters - */ -export function* generateUnmatchedDelimiters( - delimiterOccurrences: PossibleDelimiterOccurrence[], - initialIndex: number, - getCurrentAcceptableDelimiters: () => SimpleSurroundingPairName[], - lookForward: boolean, -): Generator { - /** - * This map tells us whether to increment or decrement our delimiter count - * depending on which side delimiter we see. If we're looking forward, we - * increment whenever we see a left delimiter, and decrement if we see a right - * delimiter. If we're scanning backwards, we increment whenever we see a - * right delimiter, and decrement if we see a left delimiter. - * - * We always decrement our count if side is `unknown`, (eg for a "`"). - * Otherwise we would just keep incrementing forever - */ - const delimiterIncrements: Record = lookForward - ? { - left: 1, - right: -1, - unknown: -1, - } - : { - left: -1, - right: 1, - unknown: -1, - }; - - /** - * Maps from each delimiter name to a balance indicating how many left and - * right delimiters of the given type we've seen. If this number drops to - * -1 for any delimiter, we yield it. - */ - const delimiterBalances: Partial> = - {}; - - /** - * The current list of acceptable delimiters in the ongoing scan segment. Each - * time we yield, this list might change depending on what the other direction - * found. - */ - let currentAcceptableDelimiters = getCurrentAcceptableDelimiters(); - - const indices = lookForward - ? range(initialIndex, delimiterOccurrences.length, 1) - : range(initialIndex, -1, -1); - - for (const index of indices) { - const delimiterOccurrence = delimiterOccurrences[index]; - const { delimiterInfo } = delimiterOccurrence; - const delimiterName = delimiterInfo?.delimiter; - - if ( - delimiterName == null || - !currentAcceptableDelimiters.includes(delimiterName) - ) { - continue; - } - - const increment = delimiterIncrements[delimiterInfo!.side]; - const newDelimiterBalance = - (delimiterBalances[delimiterName] ?? 0) + increment; - - if (newDelimiterBalance === -1) { - yield delimiterOccurrence as DelimiterOccurrence; - - // Refresh the list of acceptable delimiters because it may have changed - // depending on what the scan in the other direction found - currentAcceptableDelimiters = getCurrentAcceptableDelimiters(); - - // We reset the delimiter balance for the given delimiter to 0 because - // if we are continuing, it means that the scan in the opposite direction - // yielded an appropriate opposite matching delimiter. - delimiterBalances[delimiterName] = 0; - } else { - delimiterBalances[delimiterName] = newDelimiterBalance; - } - } -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/getSurroundingPairOffsets.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/getSurroundingPairOffsets.ts deleted file mode 100644 index 5a9f1a668d..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/getSurroundingPairOffsets.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { SurroundingPairOffsets, DelimiterOccurrence } from "./types"; - -/** - * Given a pair of delimiters, returns a pair of start and end offsets - * - * @param delimiter1 The first delimiter occurrence - * @param delimiter2 The second delimiter occurrence - * @returns A pair of start and end offsets for the given delimiters - */ -export function getSurroundingPairOffsets( - delimiter1: DelimiterOccurrence, - delimiter2: DelimiterOccurrence, -): SurroundingPairOffsets { - const isDelimiter1First = delimiter1.offsets.start < delimiter2.offsets.start; - const leftDelimiter = isDelimiter1First ? delimiter1 : delimiter2; - const rightDelimiter = isDelimiter1First ? delimiter2 : delimiter1; - - return { - leftDelimiter: leftDelimiter.offsets, - rightDelimiter: rightDelimiter.offsets, - }; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/index.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/index.ts deleted file mode 100644 index 635d9c1ba7..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/index.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { - ComplexSurroundingPairName, - SurroundingPairScopeType, -} from "@cursorless/common"; -import type { SyntaxNode } from "web-tree-sitter"; -import { LanguageDefinitions } from "../../../languages/LanguageDefinitions"; -import { Target } from "../../../typings/target.types"; -import { SurroundingPairTarget } from "../../targets"; -import { getContainingScopeTarget } from "../getContainingScopeTarget"; -import { complexDelimiterMap } from "./delimiterMaps"; -import { SurroundingPairInfo } from "./extractSelectionFromSurroundingPairOffsets"; -import { findSurroundingPairParseTreeBased } from "./findSurroundingPairParseTreeBased"; -import { findSurroundingPairTextBased } from "./findSurroundingPairTextBased"; - -/** - * Applies the surrounding pair modifier to the given selection. First looks to - * see if the target is itself adjacent to or contained by a modifier token. If - * so it will expand the selection to the opposite delimiter token. Otherwise, - * or if the opposite token wasn't found, it will proceed by finding the - * smallest pair of delimiters which contains the selection. - * - * @param context Context to be leveraged by modifier - * @param editor The editor containing the range - * @param range The range to process - * @param scopeType The surrounding pair modifier information - * @returns The new selection expanded to the containing surrounding pair or - * `null` if none was found - */ -export function processSurroundingPair( - languageDefinitions: LanguageDefinitions, - target: Target, - scopeType: SurroundingPairScopeType, -): SurroundingPairTarget | null { - const pairInfo = processSurroundingPairCore( - languageDefinitions, - target, - scopeType, - ); - - if (pairInfo == null) { - return null; - } - - return new SurroundingPairTarget({ - ...pairInfo, - editor: target.editor, - isReversed: target.isReversed, - }); -} - -/** - * Helper function that does the real work; caller just calls this function and - * converts output from a {@link SurroundingPairInfo} to a - * {@link SurroundingPairTarget}. - */ -function processSurroundingPairCore( - languageDefinitions: LanguageDefinitions, - target: Target, - scopeType: SurroundingPairScopeType, -): SurroundingPairInfo | null { - const { editor, contentRange: range } = target; - const languageDefinition = languageDefinitions.get( - target.editor.document.languageId, - ); - - const document = editor.document; - const delimiters = complexDelimiterMap[ - scopeType.delimiter as ComplexSurroundingPairName - ] ?? [scopeType.delimiter]; - - let node: SyntaxNode | undefined; - - try { - node = languageDefinitions.getNodeAtLocation(document, range); - - // Error nodes are unreliable and should be ignored. Fall back to text based - // algorithm. - if (node == null || nodeHasError(node)) { - return findSurroundingPairTextBased( - editor, - range, - null, - delimiters, - scopeType, - ); - } - } catch (err) { - if ((err as Error).name === "UnsupportedLanguageError") { - // If we're in a language where we don't have a parse tree we use the text - // based algorithm - return findSurroundingPairTextBased( - editor, - range, - null, - delimiters, - scopeType, - ); - } else { - throw err; - } - } - - const textFragmentRange = (() => { - // First try to use the text fragment scope handler if it exists - const textFragmentScopeHandler = languageDefinition?.getScopeHandler({ - type: "textFragment", - }); - - if (textFragmentScopeHandler != null) { - const containingScope = getContainingScopeTarget( - target, - textFragmentScopeHandler, - 0, - ); - - return containingScope?.[0].contentRange; - } - - // If we don't find a text fragment we fall back to the full document range - return document.range; - })(); - - if (textFragmentRange != null) { - // If we have a parse tree but we are in a string node or in a comment node, - // then we use the text-based algorithm - const surroundingRange = findSurroundingPairTextBased( - editor, - range, - textFragmentRange, - delimiters, - scopeType, - ); - - if (surroundingRange != null) { - return surroundingRange; - } - } - - // If we have a parse tree and either we are not in a string or comment or we - // couldn't find a surrounding pair within a string or comment, we use the - // parse tree-based algorithm - return findSurroundingPairParseTreeBased( - editor, - range, - node, - delimiters, - scopeType, - ); -} - -function nodeHasError(node: SyntaxNode, includeChildren = false): boolean { - if (nodeIsError(node)) { - return true; - } - if (includeChildren) { - if (node.children.some(nodeIsError)) { - return true; - } - } - if (node.parent != null) { - return nodeHasError(node.parent, true); - } - return false; -} - -function nodeIsError(node: SyntaxNode): boolean { - return node.type === "ERROR"; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/types.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/types.ts deleted file mode 100644 index c72e6accb0..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/types.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { SimpleSurroundingPairName } from "@cursorless/common"; - -/** - * Used to indicate whether a particular side of the delimiter is left or right - * or if we do not know. Note that the terms "opening" and "closing" could be - * used instead of "left" and "right", respectively. - */ -export type DelimiterSide = "unknown" | "left" | "right"; - -/** - * A description of one possible side of a delimiter - */ -export interface IndividualDelimiter { - /** - * The text that can be used to represent this side of the delimiter, eg "(" - */ - text: string; - - /** - * Which side of the delimiter this refers to - */ - side: DelimiterSide; - - /** - * Which delimiter this represents - */ - delimiter: SimpleSurroundingPairName; -} - -/** - * Offsets within a range or document - */ -export interface Offsets { - start: number; - end: number; -} - -/** - * The offsets of the left and right delimiter of a delimiter pair within - * range or document. - */ -export interface SurroundingPairOffsets { - leftDelimiter: Offsets; - rightDelimiter: Offsets; -} - -/** - * A possible occurrence with of a delimiter within arranger document including - * its offsets, as well as information about the delimiter itself. We allow - * `delimiterInfo` to be `null` so that implementers can lazily determine - * whether or not this is actually a delimiter, and return `null` if it is not - */ -export interface PossibleDelimiterOccurrence { - /** - * Information about the delimiter. If `null` then this delimiter occurrence - * should be ignored - */ - delimiterInfo?: IndividualDelimiter; - - /** - * The offsets of the delimiter occurrence - */ - offsets: Offsets; -} - -/** - * A confirmed occurrence of a delimiter within a document - */ -export interface DelimiterOccurrence extends PossibleDelimiterOccurrence { - delimiterInfo: IndividualDelimiter; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/weaklyContains.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/weaklyContains.ts deleted file mode 100644 index bf727be34e..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/weaklyContains.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Offsets } from "./types"; - -/** - * Determines whether {@link offsets1} weakly contains {@link offsets2}, which - * defined as the boundaries of {@link offsets1} being inside or equal to the - * boundaries of {@link offsets2}. - * @param offsets1 The first set of offsets - * @param offsets2 The second set of offsets - * @returns `true` if {@link offsets1} weakly contains {@link offsets2} - */ -export function weaklyContains(offsets1: Offsets, offsets2: Offsets) { - return offsets1.start <= offsets2.start && offsets1.end >= offsets2.end; -} diff --git a/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts b/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts index 3bea43c038..f96d963fc8 100644 --- a/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts +++ b/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts @@ -158,6 +158,7 @@ function isLanguageSpecific(scopeType: ScopeType): boolean { case "subParagraph": case "environment": case "textFragment": + case "disqualifyDelimiter": return true; case "character": diff --git a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts index 5581afdd02..6320b9e53a 100644 --- a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts +++ b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts @@ -28,6 +28,8 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { backtickQuotes: "skis", squareBrackets: "box", singleQuotes: "twin", + tripleDoubleQuotes: isPrivate("triple quad"), + tripleSingleQuotes: isPrivate("triple twin"), any: "pair", string: "string", whitespace: "void", @@ -99,6 +101,7 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { string: isPrivate("parse tree string"), textFragment: isPrivate("text fragment"), + disqualifyDelimiter: isPrivate("disqualify delimiter"), ["private.fieldAccess"]: isPrivate("access"), ["private.switchStatementSubject"]: isPrivate("subject"), }, diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeProvider/runSurroundingPairScopeInfoTest.ts b/packages/cursorless-vscode-e2e/src/suite/scopeProvider/runSurroundingPairScopeInfoTest.ts index 07024fe605..463a24088b 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeProvider/runSurroundingPairScopeInfoTest.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeProvider/runSurroundingPairScopeInfoTest.ts @@ -1,7 +1,7 @@ import { ScopeSupport, ScopeSupportInfo } from "@cursorless/common"; import { getCursorlessApi, openNewEditor } from "@cursorless/vscode-common"; import * as sinon from "sinon"; -import { commands } from "vscode"; +import { Position, commands } from "vscode"; import { assertCalledWithScopeInfo } from "./assertCalledWithScopeInfo"; /** @@ -19,8 +19,13 @@ export async function runSurroundingPairScopeInfoTest() { try { await assertCalledWithScopeInfo(fake, unsupported); - await openNewEditor(""); - await assertCalledWithScopeInfo(fake, legacy); + const editor = await openNewEditor(""); + await assertCalledWithScopeInfo(fake, supported); + + await editor.edit((editBuilder) => { + editBuilder.insert(new Position(0, 0), "()"); + }); + await assertCalledWithScopeInfo(fake, present); await commands.executeCommand("workbench.action.closeAllEditors"); await assertCalledWithScopeInfo(fake, unsupported); @@ -36,7 +41,7 @@ function getExpectedScope(scopeSupport: ScopeSupport): ScopeSupportInfo { iterationScopeSupport: scopeSupport === ScopeSupport.unsupported ? ScopeSupport.unsupported - : ScopeSupport.supportedLegacy, + : ScopeSupport.supportedAndPresentInEditor, scopeType: { type: "surroundingPair", delimiter: "parentheses" }, spokenForm: { spokenForms: ["round"], @@ -47,4 +52,5 @@ function getExpectedScope(scopeSupport: ScopeSupport): ScopeSupportInfo { } const unsupported = getExpectedScope(ScopeSupport.unsupported); -const legacy = getExpectedScope(ScopeSupport.supportedLegacy); +const supported = getExpectedScope(ScopeSupport.supportedButNotPresentInEditor); +const present = getExpectedScope(ScopeSupport.supportedAndPresentInEditor); diff --git a/packages/cursorless-vscode-e2e/src/suite/scopes.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/scopes.vscode.test.ts index ebfc9de655..aafd173f1d 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopes.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopes.vscode.test.ts @@ -104,7 +104,7 @@ async function testLanguageSupport(languageId: string, testedFacets: string[]) { async function runTest(file: string, languageId: string, facetId: string) { const { ide, scopeProvider } = (await getCursorlessApi()).testHelpers!; - const { scopeType, isIteration } = getScopeType(languageId, facetId); + const { scopeType, isIteration } = getFacetInfo(languageId, facetId); const fixture = (await fsp.readFile(file, "utf8")) .toString() .replaceAll("\r\n", "\n"); @@ -140,7 +140,7 @@ async function runTest(file: string, languageId: string, facetId: string) { const scopes = scopeProvider.provideScopeRanges(editor, config); - return serializeScopeFixture(code, scopes); + return serializeScopeFixture(facetId, code, scopes); })(); if (shouldUpdateFixtures()) { @@ -150,26 +150,28 @@ async function runTest(file: string, languageId: string, facetId: string) { } } -function getScopeType( +function getFacetInfo( languageId: string, facetId: string, ): { scopeType: ScopeType; isIteration: boolean; } { - if (languageId === "textual") { - const { scopeType, isIteration } = - textualScopeSupportFacetInfos[facetId as TextualScopeSupportFacet]; - return { - scopeType: { type: scopeType }, - isIteration: isIteration ?? false, - }; + const facetInfo = + languageId === "textual" + ? textualScopeSupportFacetInfos[facetId as TextualScopeSupportFacet] + : scopeSupportFacetInfos[facetId as ScopeSupportFacet]; + + if (facetInfo == null) { + throw Error(`Missing scope support facet info for: ${facetId}`); } - const { scopeType, isIteration } = - scopeSupportFacetInfos[facetId as ScopeSupportFacet]; + const { scopeType, isIteration } = facetInfo; + const fullScopeType = + typeof scopeType === "string" ? { type: scopeType } : scopeType; + return { - scopeType: { type: scopeType }, + scopeType: fullScopeType, isIteration: isIteration ?? false, }; } diff --git a/packages/cursorless-vscode-e2e/src/suite/serializeScopeFixture.ts b/packages/cursorless-vscode-e2e/src/suite/serializeScopeFixture.ts index fdc0c39a3b..2bc9f94fc4 100644 --- a/packages/cursorless-vscode-e2e/src/suite/serializeScopeFixture.ts +++ b/packages/cursorless-vscode-e2e/src/suite/serializeScopeFixture.ts @@ -7,15 +7,25 @@ import { import { serializeHeader } from "./serializeHeader"; import { serializeTargetRange } from "./serializeTargetRange"; +/** + * These are special facets that are really only used as scopes for debugging. + * In production we only care about the content range, so that's all we test. + */ +const contentRangeOnlyFacets = new Set(["disqualifyDelimiter"]); + export function serializeScopeFixture( + facetId: string, code: string, scopes: ScopeRanges[], ): string { const codeLines = code.split("\n"); - const serializedScopes = scopes.map((scope, index) => - serializeScope(codeLines, scope, scopes.length > 1 ? index + 1 : undefined), - ); + const serializedScopes = scopes.map((scope, index) => { + const scopeNumber = scopes.length > 1 ? index + 1 : undefined; + return contentRangeOnlyFacets.has(facetId) + ? serializeScopeContentRangeOnly(codeLines, scope, scopeNumber) + : serializeScope(codeLines, scope, scopeNumber); + }); return serializeScopeFixtureHelper(codeLines, serializedScopes); } @@ -82,6 +92,24 @@ function serializeScope( ].join("\n"); } +function serializeScopeContentRangeOnly( + codeLines: string[], + { targets }: ScopeRanges, + scopeNumber: number | undefined, +): string { + return targets + .flatMap((target, index) => [ + serializeHeader({ + header: "Content", + scopeNumber, + targetNumber: targets.length > 1 ? index + 1 : undefined, + range: target.contentRange, + }), + serializeTargetRange(codeLines, target.contentRange), + ]) + .join("\n"); +} + function serializeIterationScope( codeLines: string[], { domain, ranges }: IterationScopeRanges, @@ -222,11 +250,11 @@ function serializeTarget({ if (target.boundary != null) { lines.push( - ...target.boundary.map((interior) => + ...target.boundary.map((interior, i) => serializeTargetCompact({ codeLines, target: interior, - prefix: "Boundary", + prefix: i === 0 ? "Boundary L" : "Boundary R", scopeNumber, targetNumber, }), diff --git a/packages/cursorless-vscode-e2e/src/suite/serializeTargetRange.ts b/packages/cursorless-vscode-e2e/src/suite/serializeTargetRange.ts index 20b05af54b..618396fb29 100644 --- a/packages/cursorless-vscode-e2e/src/suite/serializeTargetRange.ts +++ b/packages/cursorless-vscode-e2e/src/suite/serializeTargetRange.ts @@ -35,8 +35,10 @@ export function serializeTargetRange( const { start, end } = range; const lines: string[] = []; + // Number of characters in the line number + `|` + const startIndent = start.line.toString().length + 1; // Add start of range marker above the first code line - const prefix = fill(" ", start.character + 2) + ">"; + const prefix = fill(" ", startIndent + start.character) + ">"; if (range.isSingleLine) { lines.push(prefix + fill("-", end.character - start.character) + "<"); } else { @@ -58,11 +60,14 @@ export function serializeTargetRange( // Add end of range marker below the last code line (if this was a multiline // range) if (!range.isSingleLine) { - lines.push(" " + fill("-", end.character) + "<"); + // Number of characters in the line number + `|` + whitespace + const endIndent = end.line.toString().length + 2; + lines.push(fill(" ", endIndent) + fill("-", end.character) + "<"); } return lines.join("\n"); } + function fill(character: string, count: number): string { return new Array(count + 1).join(character); } diff --git a/queries/c.scm b/queries/c.scm index b2e664d1f8..a9763a054f 100644 --- a/queries/c.scm +++ b/queries/c.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter/tree-sitter-c/blob/master/src/grammar.json + ;; Generated by the following command: ;; > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-cpp/master/src/node-types.json | jq '[.[] | select(.type == "compound_statement") | .children.types[].type] + [.[] | select(.type == "_statement") | .subtypes[].type]' [ @@ -243,3 +245,15 @@ "(" @argumentOrParameter.iteration.start.endOf @name.iteration.start.endOf @value.iteration.start.endOf ")" @argumentOrParameter.iteration.end.startOf @name.iteration.end.startOf @value.iteration.end.startOf ) @argumentOrParameter.iteration.domain + +operator: [ + "->" + "<" + "<<" + "<<=" + "<=" + ">" + ">=" + ">>" + ">>=" +] @disqualifyDelimiter diff --git a/queries/clojure.scm b/queries/clojure.scm index d405a55677..753d2f488e 100644 --- a/queries/clojure.scm +++ b/queries/clojure.scm @@ -1,3 +1,5 @@ +;; https://github.com/sogaiu/tree-sitter-clojure/blob/master/src/grammar.json + (comment) @comment @textFragment (str_lit) @string @textFragment diff --git a/queries/cpp.scm b/queries/cpp.scm index de5f22ba65..fda7a702ca 100644 --- a/queries/cpp.scm +++ b/queries/cpp.scm @@ -1,5 +1,7 @@ ;; import c.scm +;; https://github.com/tree-sitter/tree-sitter-cpp/blob/master/src/grammar.json + ;; Generated by the following command: ;; > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-cpp/master/src/node-types.json | jq '[.[] | select(.type == "compound_statement") | .children.types[].type] + [.[] | select(.type == "_statement") | .subtypes[].type]' [ @@ -65,3 +67,7 @@ value: (argument_list) ) @functionCall.end @_.domain.end ) + +(trailing_return_type + "->" @disqualifyDelimiter +) diff --git a/queries/csharp.scm b/queries/csharp.scm index 775a24b3eb..d5fffbef89 100644 --- a/queries/csharp.scm +++ b/queries/csharp.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter/tree-sitter-c-sharp/blob/master/src/grammar.json + ;; Generated by the following command: ;; > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-c-sharp/master/src/node-types.json \ ;; | jq '.[] | select(.type == "_statement" or .type == "_declaration") | [.subtypes[].type]' @@ -134,3 +136,25 @@ "}" @condition.iteration.end.startOf ) ) + +operator: [ + "->" + "<" + "<<" + "<=" + ">" + ">=" + ">>" +] @disqualifyDelimiter +(assignment_operator + [ + "<<=" + ">>=" + ] @disqualifyDelimiter +) +(lambda_expression + "=>" @disqualifyDelimiter +) +(member_access_expression + "->" @disqualifyDelimiter +) diff --git a/queries/css.scm b/queries/css.scm index 331583c920..2a2543227b 100644 --- a/queries/css.scm +++ b/queries/css.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter/tree-sitter-css/blob/master/src/grammar.json + (string_value) @string @textFragment (comment) @comment @textFragment @@ -26,3 +28,7 @@ "}" @name.iteration.end.startOf . ) + +(child_selector + ">" @disqualifyDelimiter +) diff --git a/queries/go.scm b/queries/go.scm index 06e0523824..73a0315319 100644 --- a/queries/go.scm +++ b/queries/go.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter/tree-sitter-go/blob/master/src/grammar.json + ;; @statement generated by the following command: ;; curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-go/master/src/node-types.json | jq '[.[] | select(.type == "_statement" or .type == "_simple_statement") | .subtypes[].type]' | grep -v '\"_' | sed -n '1d;p' | sed '$d' | sort ;; and then cleaned up. @@ -370,3 +372,18 @@ "(" @argumentOrParameter.iteration.start.endOf ")" @argumentOrParameter.iteration.end.startOf ) @argumentOrParameter.iteration.domain + +operator: [ + "<-" + "<" + "<<" + "<<=" + "<=" + ">" + ">=" + ">>" + ">>=" +] @disqualifyDelimiter +(send_statement + "<-" @disqualifyDelimiter +) diff --git a/queries/html.scm b/queries/html.scm index 2862d82fc7..70b457e14c 100644 --- a/queries/html.scm +++ b/queries/html.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter/tree-sitter-html/blob/master/src/grammar.json + ;;!! ;;! ^^^ ;;! ----- diff --git a/queries/java.scm b/queries/java.scm index dab13864ca..c438a00f71 100644 --- a/queries/java.scm +++ b/queries/java.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter/tree-sitter-java/blob/master/src/grammar.json + ;; Generated by the following command: ;; > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-java/master/src/node-types.json | jq '[.[] | select(.type == "statement" or .type == "declaration") | .subtypes[].type]' [ @@ -418,3 +420,22 @@ "(" @argumentOrParameter.iteration.start.endOf ")" @argumentOrParameter.iteration.end.startOf ) @argumentOrParameter.iteration.domain + +operator: [ + "<" + "<<" + "<<=" + "<=" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" +] @disqualifyDelimiter +(lambda_expression + "->" @disqualifyDelimiter +) +(switch_rule + "->" @disqualifyDelimiter +) diff --git a/queries/javascript.core.scm b/queries/javascript.core.scm index 922eb241a4..383c8d0a38 100644 --- a/queries/javascript.core.scm +++ b/queries/javascript.core.scm @@ -1,6 +1,8 @@ ;; import javascript.function.scm ;; import javascript.fieldAccess.scm +;; https://github.com/tree-sitter/tree-sitter-javascript/blob/master/src/grammar.json + ;; `name` scope without `export` ( (_ @@ -728,3 +730,19 @@ "(" @argumentOrParameter.iteration.start.endOf ")" @argumentOrParameter.iteration.end.startOf ) @argumentOrParameter.iteration.domain + +operator: [ + "<" + "<<" + "<<=" + "<=" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" +] @disqualifyDelimiter +(arrow_function + "=>" @disqualifyDelimiter +) diff --git a/queries/json.scm b/queries/json.scm index 05df53358e..cdcd8e2d19 100644 --- a/queries/json.scm +++ b/queries/json.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter/tree-sitter-json/blob/master/src/grammar.json + ;;!! "string" ;;! ^^^^^^ (string_content) @textFragment diff --git a/queries/latex.scm b/queries/latex.scm index 9228cecf5f..c0ce621d5c 100644 --- a/queries/latex.scm +++ b/queries/latex.scm @@ -1,3 +1,5 @@ +;; https://github.com/latex-lsp/tree-sitter-latex/blob/master/src/grammar.json + [ (block_comment) (line_comment) @@ -26,3 +28,10 @@ (end) @xmlBothTags (#allow-multiple! @xmlBothTags) ) @_.domain + +(operator + [ + "<" + ">" + ] @disqualifyDelimiter +) diff --git a/queries/lua.scm b/queries/lua.scm index 82d413e820..19ba91145f 100644 --- a/queries/lua.scm +++ b/queries/lua.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter-grammars/tree-sitter-lua/blob/main/src/grammar.json + ;; Statements [ (variable_declaration) @@ -296,3 +298,14 @@ local_declaration: (variable_declaration ;; Structures and object access ;; (method_index_expression) @private.fieldAccess + +(binary_expression + [ + "<" + "<<" + "<=" + ">" + ">=" + ">>" + ] @disqualifyDelimiter +) diff --git a/queries/markdown.scm b/queries/markdown.scm index 537bfc2ecf..bc7c8e4777 100644 --- a/queries/markdown.scm +++ b/queries/markdown.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter-grammars/tree-sitter-markdown/blob/main/src/grammar.json + (document) @textFragment (html_block) @comment diff --git a/queries/php.scm b/queries/php.scm index 18847739cc..d96bc96803 100644 --- a/queries/php.scm +++ b/queries/php.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter/tree-sitter-php/blob/master/php/src/grammar.json + ;; @statement generated by the following command: ;; curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-php/master/src/node-types.json | jq '[.[] | select(.type == "_statement" or .type == "_simple_statement") | .subtypes[].type]' | grep -v '\"_' | sed -n '1d;p' | sed '$d' | sort ;; and then cleaned up. @@ -119,3 +121,35 @@ (method_declaration name: (_) @name ) @_.domain + +operator: [ + "<" + "<<" + "<<=" + "<=" + ">" + ">=" + ">>" + ">>=" +] @disqualifyDelimiter +(array_element_initializer + "=>" @disqualifyDelimiter +) +(heredoc + "<<<" @disqualifyDelimiter +) +(nowdoc + "<<<" @disqualifyDelimiter +) +(member_access_expression + "->" @disqualifyDelimiter +) +(nullsafe_member_access_expression + "?->" @disqualifyDelimiter +) +(member_call_expression + "->" @disqualifyDelimiter +) +(nullsafe_member_call_expression + "?->" @disqualifyDelimiter +) diff --git a/queries/python.scm b/queries/python.scm index 8f236815ce..e1954e836f 100644 --- a/queries/python.scm +++ b/queries/python.scm @@ -1,5 +1,7 @@ ;; import python.fieldAccess.scm +;; https://github.com/tree-sitter/tree-sitter-python/blob/master/src/grammar.json + ;; Generated by the following command: ;; > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-python/d6210ceab11e8d812d4ab59c07c81458ec6e5184/src/node-types.json \ ;; | jq '[.[] | select(.type == "_simple_statement" or .type == "_compound_statement") | .subtypes[].type]' @@ -634,3 +636,19 @@ ")" @argumentOrParameter.iteration.end.startOf ) ) @argumentOrParameter.iteration.domain + +operators: [ + "<" + "<=" + ">" + ">=" +] @disqualifyDelimiter +operator: [ + "<<" + "<<=" + ">>" + ">>=" +] @disqualifyDelimiter +(function_definition + "->" @disqualifyDelimiter +) diff --git a/queries/ruby.scm b/queries/ruby.scm index d9a83ed8c1..99a5a5e905 100644 --- a/queries/ruby.scm +++ b/queries/ruby.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter/tree-sitter-ruby/blob/master/src/grammar.json + (comment) @comment @textFragment (hash) @map (regex) @regularExpression @@ -50,3 +52,20 @@ (operator_assignment left: (_) @name ) @_.domain + +operator: [ + "<" + "<<" + "<<=" + "<=" + ">" + ">=" + ">>" + ">>=" +] @disqualifyDelimiter +(pair + "=>" @disqualifyDelimiter +) +(match_pattern + "=>" @disqualifyDelimiter +) diff --git a/queries/rust.scm b/queries/rust.scm index 97482c24b5..a49b0aba0f 100644 --- a/queries/rust.scm +++ b/queries/rust.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter/tree-sitter-rust/blob/master/src/grammar.json + [ (if_expression) (if_let_expression) @@ -55,3 +57,23 @@ (match_expression value: (_) @private.switchStatementSubject ) @_.domain + +operator: [ + "<" + "<<" + "<<=" + "<=" + ">" + ">=" + ">>" + ">>=" +] @disqualifyDelimiter +(function_item + "->" @disqualifyDelimiter +) +(match_arm + "=>" @disqualifyDelimiter +) +(macro_rule + "=>" @disqualifyDelimiter +) diff --git a/queries/scala.scm b/queries/scala.scm index 1e86123c6f..748cfc2f9e 100644 --- a/queries/scala.scm +++ b/queries/scala.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter/tree-sitter-scala/blob/master/src/grammar.json + (if_expression) @ifStatement [ @@ -39,3 +41,26 @@ (_ pattern: (_) @name ) @_.domain + +operator: (operator_identifier) @disqualifyDelimiter +(enumerator + "<-" @disqualifyDelimiter +) +(view_bound + "<%" @disqualifyDelimiter +) +(upper_bound + "<:" @disqualifyDelimiter +) +(lower_bound + ">:" @disqualifyDelimiter +) +(lambda_expression + "=>" @disqualifyDelimiter +) +(function_type + "=>" @disqualifyDelimiter +) +(case_clause + "=>" @disqualifyDelimiter +) diff --git a/queries/scm.scm b/queries/scm.scm index 66ebad40e6..737fea6129 100644 --- a/queries/scm.scm +++ b/queries/scm.scm @@ -1,6 +1,8 @@ ;; import scm.collections.scm ;; import scm.name.scm +;; https://github.com/tree-sitter-grammars/tree-sitter-query/blob/master/src/grammar.json + ;; A statement is any top-level node that's not a comment ( (program diff --git a/queries/scss.scm b/queries/scss.scm index 6f76a4091f..6cbdf48334 100644 --- a/queries/scss.scm +++ b/queries/scss.scm @@ -1,5 +1,7 @@ ;; import css.scm +;; https://github.com/serenadeai/tree-sitter-scss/blob/master/src/grammar.json + (single_line_comment) @comment @textFragment (if_statement) @ifStatement @@ -26,3 +28,12 @@ "}" @namedFunction.iteration.end.startOf @functionName.iteration.end.startOf . ) + +(binary_expression + [ + "<" + "<=" + ">" + ">=" + ] @disqualifyDelimiter +) diff --git a/queries/talon.scm b/queries/talon.scm index 884e7b94ff..726533ffac 100644 --- a/queries/talon.scm +++ b/queries/talon.scm @@ -1,3 +1,5 @@ +;; https://github.com/pokey/tree-sitter-talon/blob/dev/src/grammar.json + ;;!! foo: "bar" ;;! ^^^^^^^^^^ ;;!! edit.left() diff --git a/queries/xml.scm b/queries/xml.scm index 066879cc15..ec7b93628b 100644 --- a/queries/xml.scm +++ b/queries/xml.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter-grammars/tree-sitter-xml/blob/master/xml/src/grammar.json + ;;!! ;;! ^^^ ;;! ----- diff --git a/queries/yaml.scm b/queries/yaml.scm index d7f12db3cc..4b950da845 100644 --- a/queries/yaml.scm +++ b/queries/yaml.scm @@ -1,3 +1,5 @@ +;; https://github.com/tree-sitter-grammars/tree-sitter-yaml/blob/master/src/grammar.json + ;; ;;!! foo: bar ;; ;;! ^^^ ^^^ (_ @@ -75,3 +77,7 @@ ;;!! # comment ;;! ^^^^^^^^^ (comment) @comment @textFragment + +(block_scalar + ">" @disqualifyDelimiter +)