From 286260c49eadc2a2fdd12341e9fa12bcc6df2097 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 31 Dec 2024 13:31:53 +0100 Subject: [PATCH 01/10] Proper handling of surrounding pair delimiter prefixes --- .../parseTree/python/changeInside.yml | 22 ++++++++++++ .../{changePair2.yml => changePair3.yml} | 6 ++-- .../delimiterMaps.ts | 36 ++++--------------- .../getDelimiterOccurrences.ts | 30 ++++++++++++---- .../getDelimiterRegex.ts | 20 ++++++++--- .../getIndividualDelimiters.ts | 8 ++--- .../SurroundingPairScopeHandler/types.ts | 5 +++ 7 files changed, 79 insertions(+), 48 deletions(-) create mode 100644 data/fixtures/recorded/surroundingPair/parseTree/python/changeInside.yml rename data/fixtures/recorded/surroundingPair/parseTree/python/{changePair2.yml => changePair3.yml} (79%) diff --git a/data/fixtures/recorded/surroundingPair/parseTree/python/changeInside.yml b/data/fixtures/recorded/surroundingPair/parseTree/python/changeInside.yml new file mode 100644 index 0000000000..62718ad5b6 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/parseTree/python/changeInside.yml @@ -0,0 +1,22 @@ +languageId: python +command: + version: 7 + spokenForm: change inside + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - {type: interiorOnly} + usePrePhraseSnapshot: true +initialState: + documentContents: r'command server' + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: r'' + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} diff --git a/data/fixtures/recorded/surroundingPair/parseTree/python/changePair2.yml b/data/fixtures/recorded/surroundingPair/parseTree/python/changePair3.yml similarity index 79% rename from data/fixtures/recorded/surroundingPair/parseTree/python/changePair2.yml rename to data/fixtures/recorded/surroundingPair/parseTree/python/changePair3.yml index 99faeae3d1..1969b7a28c 100644 --- a/data/fixtures/recorded/surroundingPair/parseTree/python/changePair2.yml +++ b/data/fixtures/recorded/surroundingPair/parseTree/python/changePair3.yml @@ -11,10 +11,10 @@ command: scopeType: {type: surroundingPair, delimiter: any} usePrePhraseSnapshot: true initialState: - documentContents: "\" r\"" + documentContents: r'command server' selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} marks: {} finalState: documentContents: "" diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts index 3b55057c5a..d3e47ef0fb 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts @@ -16,15 +16,9 @@ interface Options { 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. + * The prefixes that can be used before the left side of the delimiter, eg "r" */ - isUnknownSide?: boolean; + prefixes?: string[]; } type DelimiterMap = Record< @@ -54,8 +48,6 @@ const delimiterToText: DelimiterMap = Object.freeze({ // https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals const pythonPrefixes = [ - // Base case without a prefix - "", // string prefixes "r", "u", @@ -102,26 +94,10 @@ const delimiterToTextOverrides: Record> = { }, python: { - 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 }, - ], + singleQuotes: ["'", "'", { isSingleLine: true, prefixes: pythonPrefixes }], + doubleQuotes: ['"', '"', { isSingleLine: true, prefixes: pythonPrefixes }], + tripleSingleQuotes: ["'''", "'''", { prefixes: pythonPrefixes }], + tripleDoubleQuotes: ['"""', '"""', { prefixes: pythonPrefixes }], }, ruby: { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts index 18f42e3845..2d29803c43 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts @@ -27,12 +27,8 @@ export function getDelimiterOccurrences( const textFragments = languageDefinition?.getCaptures(document, "textFragment") ?? []; - const delimiterTextToDelimiterInfoMap = Object.fromEntries( - individualDelimiters.map((individualDelimiter) => [ - individualDelimiter.text, - individualDelimiter, - ]), - ); + const delimiterTextToDelimiterInfoMap = + getDelimiterTextToDelimiterInfoMap(individualDelimiters); const text = document.getText(); @@ -59,3 +55,25 @@ export function getDelimiterOccurrences( }; }); } + +function getDelimiterTextToDelimiterInfoMap( + individualDelimiters: IndividualDelimiter[], +): Record { + return Object.fromEntries( + individualDelimiters.flatMap((individualDelimiter) => { + const results = [[individualDelimiter.text, individualDelimiter]]; + if (individualDelimiter.prefixes.length > 0) { + for (const prefix of individualDelimiter.prefixes) { + const prefixText = prefix + individualDelimiter.text; + const prefixDelimiter: IndividualDelimiter = { + ...individualDelimiter, + text: prefixText, + side: "left", + }; + results.push([prefixText, prefixDelimiter]); + } + } + return results; + }), + ); +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts index 5c6e0309f5..771a4c64aa 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts @@ -12,11 +12,23 @@ 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("|"); + individualDelimiters.flatMap((delimiter) => { + const text = escapeRegExp(delimiter.text); + const result = [text]; + for (const prefix of delimiter.prefixes) { + // If the prefix is only alpha character, we need to make sure that there is no preceding alpha characters. + if (alphaRegex.test(prefix)) { + result.push(`(? { const [leftDelimiter, rightDelimiter, options] = delimiterToText[delimiterName]; - const { isSingleLine = false, isUnknownSide = false } = options ?? {}; + const { isSingleLine = false, prefixes = [] } = options ?? {}; // Allow for the fact that a delimiter might have multiple ways to indicate // its opening / closing @@ -54,9 +54,6 @@ function getSimpleIndividualDelimiters( const isRight = rightDelimiters.includes(text); const side = (() => { - if (isUnknownSide) { - return "unknown"; - } if (isLeft && !isRight) { return "left"; } @@ -73,6 +70,7 @@ function getSimpleIndividualDelimiters( side, delimiterName, isSingleLine, + prefixes, }; }); }); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/types.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/types.ts index 923f0e250a..c7f31a6686 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/types.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/types.ts @@ -28,6 +28,11 @@ export interface IndividualDelimiter { */ isSingleLine: boolean; + /** + * The prefixes that can be used before the left side of the delimiter, eg "r" + */ + prefixes: string[]; + /** * The text that can be used to represent this side of the delimiter, eg "(" */ From 1848a375819036bb915a43107281e10ab7de39fd Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 31 Dec 2024 13:36:14 +0100 Subject: [PATCH 02/10] cleanup --- .../getDelimiterOccurrences.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts index 2d29803c43..8a0e121e2a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts @@ -62,16 +62,14 @@ function getDelimiterTextToDelimiterInfoMap( return Object.fromEntries( individualDelimiters.flatMap((individualDelimiter) => { const results = [[individualDelimiter.text, individualDelimiter]]; - if (individualDelimiter.prefixes.length > 0) { - for (const prefix of individualDelimiter.prefixes) { - const prefixText = prefix + individualDelimiter.text; - const prefixDelimiter: IndividualDelimiter = { - ...individualDelimiter, - text: prefixText, - side: "left", - }; - results.push([prefixText, prefixDelimiter]); - } + for (const prefix of individualDelimiter.prefixes) { + const prefixText = prefix + individualDelimiter.text; + const prefixDelimiter: IndividualDelimiter = { + ...individualDelimiter, + text: prefixText, + side: "left", + }; + results.push([prefixText, prefixDelimiter]); } return results; }), From 111dd92c2224ededebe4d8facefe890587093c69 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 7 Jan 2025 15:56:41 +0100 Subject: [PATCH 03/10] Update packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com> --- .../scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts index d3e47ef0fb..e536df256f 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts @@ -16,7 +16,8 @@ interface Options { isSingleLine?: boolean; /** - * The prefixes that can be used before the left side of the delimiter, eg "r" + * The prefixes that can be used before the left side of the delimiter, eg "r". + * Note that the empty string is always considered an acceptable prefix */ prefixes?: string[]; } From 0b2b1580eb11fe1b47189dd7d409a065f34ec4d7 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 7 Jan 2025 16:39:35 +0100 Subject: [PATCH 04/10] Re add failing test --- .../parseTree/python/changePair2.yml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 data/fixtures/recorded/surroundingPair/parseTree/python/changePair2.yml diff --git a/data/fixtures/recorded/surroundingPair/parseTree/python/changePair2.yml b/data/fixtures/recorded/surroundingPair/parseTree/python/changePair2.yml new file mode 100644 index 0000000000..a7c42ed0a5 --- /dev/null +++ b/data/fixtures/recorded/surroundingPair/parseTree/python/changePair2.yml @@ -0,0 +1,23 @@ +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: "' r'" + 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} From d861d83ea27265701cf5259d00af10d404023073 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 8 Jan 2025 15:37:03 +0100 Subject: [PATCH 05/10] Use parse tree --- cursorless-talon/src/spoken_forms.py | 1 + .../scopes/python/pairDelimiter.scope | 72 +++++++++++++++++++ .../common/src/scopeSupportFacets/python.ts | 1 + .../scopeSupportFacetInfos.ts | 5 ++ .../scopeSupportFacets.types.ts | 1 + .../command/PartialTargetDescriptor.types.ts | 1 + .../queryPredicateOperators.ts | 16 +++++ .../delimiterMaps.ts | 42 +---------- .../getDelimiterOccurrences.ts | 63 ++++++++-------- .../getDelimiterRegex.ts | 20 ++---- .../getIndividualDelimiters.ts | 3 +- .../SurroundingPairScopeHandler/types.ts | 5 -- .../src/scopeProviders/ScopeInfoProvider.ts | 1 + .../spokenForms/defaultSpokenFormMapCore.ts | 1 + queries/python.scm | 5 ++ 15 files changed, 140 insertions(+), 97 deletions(-) create mode 100644 data/fixtures/scopes/python/pairDelimiter.scope diff --git a/cursorless-talon/src/spoken_forms.py b/cursorless-talon/src/spoken_forms.py index 1b89cd96c5..5d66e1ffc1 100644 --- a/cursorless-talon/src/spoken_forms.py +++ b/cursorless-talon/src/spoken_forms.py @@ -160,6 +160,7 @@ def handle_new_values(csv_name: str, values: list[SpokenFormEntry]): "private.switchStatementSubject", "textFragment", "disqualifyDelimiter", + "pairDelimiter", ], default_list_name="scope_type", ), diff --git a/data/fixtures/scopes/python/pairDelimiter.scope b/data/fixtures/scopes/python/pairDelimiter.scope new file mode 100644 index 0000000000..ff8c087dfa --- /dev/null +++ b/data/fixtures/scopes/python/pairDelimiter.scope @@ -0,0 +1,72 @@ +"server" +'server' +"""server""" +'''server''' +r" r" +r' r' +r""" r""" +r''' r''' +--- + +[#1 Content] = +[#1 Domain] = 4:0-4:2 + >--< +4| r" r" + +[#1 Removal] = 4:0-4:3 + >---< +4| r" r" + +[#1 Trailing delimiter] = 4:2-4:3 + >-< +4| r" r" + +[#1 Insertion delimiter] = " " + + +[#2 Content] = +[#2 Domain] = 5:0-5:2 + >--< +5| r' r' + +[#2 Removal] = 5:0-5:3 + >---< +5| r' r' + +[#2 Trailing delimiter] = 5:2-5:3 + >-< +5| r' r' + +[#2 Insertion delimiter] = " " + + +[#3 Content] = +[#3 Domain] = 6:0-6:4 + >----< +6| r""" r""" + +[#3 Removal] = 6:0-6:5 + >-----< +6| r""" r""" + +[#3 Trailing delimiter] = 6:4-6:5 + >-< +6| r""" r""" + +[#3 Insertion delimiter] = " " + + +[#4 Content] = +[#4 Domain] = 7:0-7:4 + >----< +7| r''' r''' + +[#4 Removal] = 7:0-7:5 + >-----< +7| r''' r''' + +[#4 Trailing delimiter] = 7:4-7:5 + >-< +7| r''' r''' + +[#4 Insertion delimiter] = " " diff --git a/packages/common/src/scopeSupportFacets/python.ts b/packages/common/src/scopeSupportFacets/python.ts index 7fdac4b435..05f5bd400b 100644 --- a/packages/common/src/scopeSupportFacets/python.ts +++ b/packages/common/src/scopeSupportFacets/python.ts @@ -14,6 +14,7 @@ export const pythonScopeSupport: LanguageScopeSupportFacetMap = { namedFunction: supported, anonymousFunction: supported, disqualifyDelimiter: supported, + pairDelimiter: supported, "argument.actual": supported, "argument.actual.iteration": supported, diff --git a/packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts b/packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts index 8ac41be384..8102ca4d8e 100644 --- a/packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts +++ b/packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts @@ -298,6 +298,11 @@ export const scopeSupportFacetInfos: Record< "Used to disqualify a token from being treated as a surrounding pair delimiter. This will usually be operators containing `>` or `<`, eg `<`, `<=`, `->`, etc", scopeType: "disqualifyDelimiter", }, + pairDelimiter: { + description: + "A pair delimiter, eg parentheses, brackets, braces, quotes, etc", + scopeType: "pairDelimiter", + }, "branch.if": { description: "An if/elif/else branch", diff --git a/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts index 638a1bea20..40728f4a9a 100644 --- a/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts +++ b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts @@ -80,6 +80,7 @@ export const scopeSupportFacets = [ "textFragment.string.multiLine", "disqualifyDelimiter", + "pairDelimiter", "branch.if", "branch.if.iteration", diff --git a/packages/common/src/types/command/PartialTargetDescriptor.types.ts b/packages/common/src/types/command/PartialTargetDescriptor.types.ts index a382bc9dc2..e09d94d230 100644 --- a/packages/common/src/types/command/PartialTargetDescriptor.types.ts +++ b/packages/common/src/types/command/PartialTargetDescriptor.types.ts @@ -205,6 +205,7 @@ export const simpleScopeTypeTypes = [ // Private scope types "textFragment", "disqualifyDelimiter", + "pairDelimiter", ] as const; export function isSimpleScopeType( diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts index 143edece3b..1ba2bca944 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts @@ -62,6 +62,21 @@ class HasMultipleChildrenOfType extends QueryPredicateOperator { + name = "match?" as const; + schema = z.tuple([q.node, q.string]); + + run(nodeInfo: MutableQueryCapture, pattern: string) { + const { document, range } = nodeInfo; + const regex = new RegExp(pattern, "ds"); + const text = document.getText(range); + return regex.test(text); + } +} + class ChildRange extends QueryPredicateOperator { name = "child-range!" as const; schema = z.union([ @@ -277,4 +292,5 @@ export const queryPredicateOperators = [ new InsertionDelimiter(), new SingleOrMultilineDelimiter(), new HasMultipleChildrenOfType(), + new Match(), ]; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts index e536df256f..d82149625f 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts @@ -14,12 +14,6 @@ interface Options { * salient example is strings. */ isSingleLine?: boolean; - - /** - * The prefixes that can be used before the left side of the delimiter, eg "r". - * Note that the empty string is always considered an acceptable prefix - */ - prefixes?: string[]; } type DelimiterMap = Record< @@ -47,36 +41,6 @@ const delimiterToText: DelimiterMap = Object.freeze({ squareBrackets: ["[", "]"], }); -// https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals -const pythonPrefixes = [ - // 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> = { @@ -95,10 +59,8 @@ const delimiterToTextOverrides: Record> = { }, python: { - singleQuotes: ["'", "'", { isSingleLine: true, prefixes: pythonPrefixes }], - doubleQuotes: ['"', '"', { isSingleLine: true, prefixes: pythonPrefixes }], - tripleSingleQuotes: ["'''", "'''", { prefixes: pythonPrefixes }], - tripleDoubleQuotes: ['"""', '"""', { prefixes: pythonPrefixes }], + tripleSingleQuotes: ["'''", "'''"], + tripleDoubleQuotes: ['"""', '"""'], }, ruby: { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts index 55880fb594..94302e5569 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts @@ -31,13 +31,19 @@ export function getDelimiterOccurrences( const disqualifyDelimiters = new OneWayRangeFinder( getSortedCaptures(languageDefinition, document, "disqualifyDelimiter"), ); - // We need a tree for text fragments since they can be nested + const pairDelimiters = new OneWayRangeFinder( + getSortedCaptures(languageDefinition, document, "pairDelimiter"), + ); const textFragments = new OneWayNestedRangeFinder( getSortedCaptures(languageDefinition, document, "textFragment"), ); - const delimiterTextToDelimiterInfoMap = - getDelimiterTextToDelimiterInfoMap(individualDelimiters); + const delimiterTextToDelimiterInfoMap = Object.fromEntries( + individualDelimiters.map((individualDelimiter) => [ + individualDelimiter.text, + individualDelimiter, + ]), + ); const regexMatches = matchAllIterator( document.getText(), @@ -48,28 +54,37 @@ export function getDelimiterOccurrences( for (const match of regexMatches) { const text = match[0]; - const range = new Range( + const matchRange = new Range( document.positionAt(match.index!), document.positionAt(match.index! + text.length), ); - const delimiter = disqualifyDelimiters.getContaining(range); - const isDisqualified = delimiter != null && !delimiter.hasError(); + const disqualifiedDelimiter = ifNoErrors( + disqualifyDelimiters.getContaining(matchRange), + ); - if (!isDisqualified) { - const textFragmentRange = - textFragments.getSmallestContaining(range)?.range; - results.push({ - delimiterInfo: delimiterTextToDelimiterInfoMap[text], - textFragmentRange, - range, - }); + if (disqualifiedDelimiter != null) { + continue; } + + results.push({ + delimiterInfo: delimiterTextToDelimiterInfoMap[text], + textFragmentRange: ifNoErrors( + textFragments.getSmallestContaining(matchRange), + )?.range, + range: + ifNoErrors(pairDelimiters.getContaining(matchRange))?.range ?? + matchRange, + }); } return results; } +function ifNoErrors(capture?: QueryCapture): QueryCapture | undefined { + return capture != null && !capture.hasError() ? capture : undefined; +} + function getSortedCaptures( languageDefinition: LanguageDefinition | undefined, document: TextDocument, @@ -79,23 +94,3 @@ function getSortedCaptures( items.sort((a, b) => a.range.start.compareTo(b.range.start)); return items; } - -function getDelimiterTextToDelimiterInfoMap( - individualDelimiters: IndividualDelimiter[], -): Record { - return Object.fromEntries( - individualDelimiters.flatMap((individualDelimiter) => { - const results = [[individualDelimiter.text, individualDelimiter]]; - for (const prefix of individualDelimiter.prefixes) { - const prefixText = prefix + individualDelimiter.text; - const prefixDelimiter: IndividualDelimiter = { - ...individualDelimiter, - text: prefixText, - side: "left", - }; - results.push([prefixText, prefixDelimiter]); - } - return results; - }), - ); -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts index 771a4c64aa..5c6e0309f5 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts @@ -12,23 +12,11 @@ export function getDelimiterRegex(individualDelimiters: IndividualDelimiter[]) { // Create a regex which is a disjunction of all possible left / right // delimiter texts const individualDelimiterDisjunct = uniq( - individualDelimiters.flatMap((delimiter) => { - const text = escapeRegExp(delimiter.text); - const result = [text]; - for (const prefix of delimiter.prefixes) { - // If the prefix is only alpha character, we need to make sure that there is no preceding alpha characters. - if (alphaRegex.test(prefix)) { - result.push(`(? text), + ) + .map(escapeRegExp) + .join("|"); // Then make sure that we don't allow preceding `\` return new RegExp(`(? { const [leftDelimiter, rightDelimiter, options] = delimiterToText[delimiterName]; - const { isSingleLine = false, prefixes = [] } = options ?? {}; + const { isSingleLine = false } = options ?? {}; // Allow for the fact that a delimiter might have multiple ways to indicate // its opening / closing @@ -70,7 +70,6 @@ function getSimpleIndividualDelimiters( side, delimiterName, isSingleLine, - prefixes, }; }); }); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/types.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/types.ts index 70e3666432..253b8debdd 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/types.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/types.ts @@ -28,11 +28,6 @@ export interface IndividualDelimiter { */ isSingleLine: boolean; - /** - * The prefixes that can be used before the left side of the delimiter, eg "r" - */ - prefixes: string[]; - /** * The text that can be used to represent this side of the delimiter, eg "(" */ diff --git a/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts b/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts index 7578a5195b..a1b982a5cd 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 "environment": case "textFragment": case "disqualifyDelimiter": + case "pairDelimiter": return true; case "character": diff --git a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts index 7717823ec9..387c2adbb7 100644 --- a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts +++ b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts @@ -103,6 +103,7 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { string: isPrivate("parse tree string"), textFragment: isPrivate("text fragment"), disqualifyDelimiter: isPrivate("disqualify delimiter"), + pairDelimiter: isPrivate("pair delimiter"), ["private.fieldAccess"]: isPrivate("access"), ["private.switchStatementSubject"]: isPrivate("subject"), }, diff --git a/queries/python.scm b/queries/python.scm index e1954e836f..5e15bc5e13 100644 --- a/queries/python.scm +++ b/queries/python.scm @@ -652,3 +652,8 @@ operator: [ (function_definition "->" @disqualifyDelimiter ) + +( + (string_start) @pairDelimiter + (#match? @pairDelimiter "^\\w") +) From af0205d03563ec0a5c32b0ec26140a2832c17553 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 8 Jan 2025 16:00:44 +0100 Subject: [PATCH 06/10] Update test --- .../surroundingPair/parseTree/typescript/changePair.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/fixtures/recorded/surroundingPair/parseTree/typescript/changePair.yml b/data/fixtures/recorded/surroundingPair/parseTree/typescript/changePair.yml index f9187f9fa4..0570861d55 100644 --- a/data/fixtures/recorded/surroundingPair/parseTree/typescript/changePair.yml +++ b/data/fixtures/recorded/surroundingPair/parseTree/typescript/changePair.yml @@ -12,9 +12,9 @@ command: usePrePhraseSnapshot: true initialState: documentContents: |- - ( - // ) - ) + { + // } + } selections: - anchor: {line: 0, character: 1} active: {line: 0, character: 1} From 85f92eb52255e492c64eff53d0c643b580da95f5 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 10 Jan 2025 12:15:11 +0100 Subject: [PATCH 07/10] Restore test --- .../surroundingPair/parseTree/typescript/changePair.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/fixtures/recorded/surroundingPair/parseTree/typescript/changePair.yml b/data/fixtures/recorded/surroundingPair/parseTree/typescript/changePair.yml index 0570861d55..f9187f9fa4 100644 --- a/data/fixtures/recorded/surroundingPair/parseTree/typescript/changePair.yml +++ b/data/fixtures/recorded/surroundingPair/parseTree/typescript/changePair.yml @@ -12,9 +12,9 @@ command: usePrePhraseSnapshot: true initialState: documentContents: |- - { - // } - } + ( + // ) + ) selections: - anchor: {line: 0, character: 1} active: {line: 0, character: 1} From 9a4b27daac9cecd263a5c20f169a19593ad6cf31 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 10 Jan 2025 12:17:24 +0100 Subject: [PATCH 08/10] Allow text fragments with error --- .../SurroundingPairScopeHandler/getDelimiterOccurrences.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts index f80afeea5a..a7ad5b51cb 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts @@ -65,9 +65,7 @@ export function getDelimiterOccurrences( results.push({ delimiterInfo: delimiterTextToDelimiterInfoMap[text], - textFragmentRange: ifNoErrors( - textFragments.getSmallestContaining(matchRange), - )?.range, + textFragmentRange: textFragments.getSmallestContaining(matchRange)?.range, range: ifNoErrors(pairDelimiters.getContaining(matchRange))?.range ?? matchRange, From 2758c06c22f17469aa45507958f224f1c1bddc0c Mon Sep 17 00:00:00 2001 From: Phil Cohen Date: Fri, 10 Jan 2025 09:35:02 -0800 Subject: [PATCH 09/10] Update queries/python.scm --- queries/python.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queries/python.scm b/queries/python.scm index 5e15bc5e13..53aafb108d 100644 --- a/queries/python.scm +++ b/queries/python.scm @@ -655,5 +655,5 @@ operator: [ ( (string_start) @pairDelimiter - (#match? @pairDelimiter "^\\w") + (#match? @pairDelimiter "^[a-zA-z]+") ) From 63121fc2a6f679909605fd2d36ba08739a8266d5 Mon Sep 17 00:00:00 2001 From: Phil Cohen Date: Fri, 10 Jan 2025 09:35:18 -0800 Subject: [PATCH 10/10] Update queries/python.scm --- queries/python.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queries/python.scm b/queries/python.scm index 53aafb108d..f5e61291d0 100644 --- a/queries/python.scm +++ b/queries/python.scm @@ -655,5 +655,5 @@ operator: [ ( (string_start) @pairDelimiter - (#match? @pairDelimiter "^[a-zA-z]+") + (#match? @pairDelimiter "^[a-zA-Z]+") )