diff --git a/data/fixtures/scopes/java/comment.block.scope b/data/fixtures/scopes/java/comment.block.scope new file mode 100644 index 0000000000..f8b38ca6af --- /dev/null +++ b/data/fixtures/scopes/java/comment.block.scope @@ -0,0 +1,15 @@ +/* + Hello world +*/ +--- + +[Content] = +[Removal] = +[Domain] = 0:0-2:2 + >-- +0| /* +1| Hello world +2| */ + --< + +[Insertion delimiter] = "\n" diff --git a/data/fixtures/scopes/java/comment.block2.scope b/data/fixtures/scopes/java/comment.block2.scope new file mode 100644 index 0000000000..a0ab76da25 --- /dev/null +++ b/data/fixtures/scopes/java/comment.block2.scope @@ -0,0 +1,15 @@ +/** +* Hello world +*/ +--- + +[Content] = +[Removal] = +[Domain] = 0:0-2:2 + >--- +0| /** +1| * Hello world +2| */ + --< + +[Insertion delimiter] = "\n" diff --git a/data/fixtures/scopes/java/comment.block3.scope b/data/fixtures/scopes/java/comment.block3.scope new file mode 100644 index 0000000000..b4fbefaaaa --- /dev/null +++ b/data/fixtures/scopes/java/comment.block3.scope @@ -0,0 +1,13 @@ +// Hello +// World +--- + +[Content] = +[Removal] = +[Domain] = 0:0-1:8 + >-------- +0| // Hello +1| // World + --------< + +[Insertion delimiter] = "\n" diff --git a/data/fixtures/scopes/java/comment.line.scope b/data/fixtures/scopes/java/comment.line.scope new file mode 100644 index 0000000000..7d1477b8a1 --- /dev/null +++ b/data/fixtures/scopes/java/comment.line.scope @@ -0,0 +1,10 @@ +// Hello world +--- + +[Content] = +[Removal] = +[Domain] = 0:0-0:14 + >--------------< +0| // Hello world + +[Insertion delimiter] = "\n" diff --git a/data/fixtures/scopes/javascript.core/comment.block3.scope b/data/fixtures/scopes/javascript.core/comment.block3.scope new file mode 100644 index 0000000000..b4fbefaaaa --- /dev/null +++ b/data/fixtures/scopes/javascript.core/comment.block3.scope @@ -0,0 +1,13 @@ +// Hello +// World +--- + +[Content] = +[Removal] = +[Domain] = 0:0-1:8 + >-------- +0| // Hello +1| // World + --------< + +[Insertion delimiter] = "\n" diff --git a/data/fixtures/scopes/python/comment.block.scope b/data/fixtures/scopes/python/comment.block.scope new file mode 100644 index 0000000000..6d40f81d29 --- /dev/null +++ b/data/fixtures/scopes/python/comment.block.scope @@ -0,0 +1,13 @@ +# Hello +# World +--- + +[Content] = +[Removal] = +[Domain] = 0:0-1:7 + >------- +0| # Hello +1| # World + -------< + +[Insertion delimiter] = "\n" diff --git a/data/fixtures/scopes/python/comment.line.scope b/data/fixtures/scopes/python/comment.line.scope new file mode 100644 index 0000000000..42719cdbb8 --- /dev/null +++ b/data/fixtures/scopes/python/comment.line.scope @@ -0,0 +1,10 @@ +# Hello world +--- + +[Content] = +[Removal] = +[Domain] = 0:0-0:13 + >-------------< +0| # Hello world + +[Insertion delimiter] = "\n" diff --git a/data/fixtures/scopes/talon/comment.block.scope b/data/fixtures/scopes/talon/comment.block.scope new file mode 100644 index 0000000000..6d40f81d29 --- /dev/null +++ b/data/fixtures/scopes/talon/comment.block.scope @@ -0,0 +1,13 @@ +# Hello +# World +--- + +[Content] = +[Removal] = +[Domain] = 0:0-1:7 + >------- +0| # Hello +1| # World + -------< + +[Insertion delimiter] = "\n" diff --git a/data/fixtures/scopes/talon/comment.line.scope b/data/fixtures/scopes/talon/comment.line.scope new file mode 100644 index 0000000000..42719cdbb8 --- /dev/null +++ b/data/fixtures/scopes/talon/comment.line.scope @@ -0,0 +1,10 @@ +# Hello world +--- + +[Content] = +[Removal] = +[Domain] = 0:0-0:13 + >-------------< +0| # Hello world + +[Insertion delimiter] = "\n" diff --git a/packages/common/src/scopeSupportFacets/java.ts b/packages/common/src/scopeSupportFacets/java.ts index de6e28978c..e54d162096 100644 --- a/packages/common/src/scopeSupportFacets/java.ts +++ b/packages/common/src/scopeSupportFacets/java.ts @@ -23,6 +23,9 @@ export const javaScopeSupport: LanguageScopeSupportFacetMap = { "argument.actual": supported, "argument.actual.iteration": supported, + "comment.line": supported, + "comment.block": supported, + element: notApplicable, tags: notApplicable, attribute: notApplicable, diff --git a/packages/common/src/scopeSupportFacets/python.ts b/packages/common/src/scopeSupportFacets/python.ts index 21080c34f5..e146e549b9 100644 --- a/packages/common/src/scopeSupportFacets/python.ts +++ b/packages/common/src/scopeSupportFacets/python.ts @@ -23,6 +23,8 @@ export const pythonScopeSupport: LanguageScopeSupportFacetMap = { "argument.formal": supportedLegacy, "argument.formal.iteration": supportedLegacy, + "comment.line": supported, + "comment.block": supported, "branch.if": supported, "branch.if.iteration": supported, "branch.switchCase": supported, diff --git a/packages/common/src/scopeSupportFacets/talon.ts b/packages/common/src/scopeSupportFacets/talon.ts index b9cff24296..e4aae83ca6 100644 --- a/packages/common/src/scopeSupportFacets/talon.ts +++ b/packages/common/src/scopeSupportFacets/talon.ts @@ -9,4 +9,7 @@ const { supported } = ScopeSupportFacetLevel; export const talonScopeSupport: LanguageScopeSupportFacetMap = { command: supported, + + "comment.line": supported, + "comment.block": supported, }; diff --git a/packages/common/src/util/itertools.ts b/packages/common/src/util/itertools.ts index 435b3d044c..cf5342fe61 100644 --- a/packages/common/src/util/itertools.ts +++ b/packages/common/src/util/itertools.ts @@ -65,3 +65,17 @@ export function isEmptyIterable(iterable: Iterable): boolean { return true; } + +/** + * Returns the first element of the given iterable, or `undefined` if the + * iterable is empty + * @param iterable The iterable to get the first element of + * @returns The first element of the iterable, or `undefined` if the iterable + * is empty + */ +export function next(generator: Iterable): T | undefined { + for (const value of generator) { + return value; + } + return undefined; +} diff --git a/packages/cursorless-engine/src/languages/LanguageDefinition.ts b/packages/cursorless-engine/src/languages/LanguageDefinition.ts index 286358d9b8..9aab433689 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinition.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinition.ts @@ -7,10 +7,12 @@ import { import { basename, dirname, join } from "path"; import { TreeSitterScopeHandler } from "../processTargets/modifiers/scopeHandlers"; import { ide } from "../singletons/ide.singleton"; -import { TreeSitter } from "../typings/TreeSitter"; +import type { TreeSitter } from "../typings/TreeSitter"; import { matchAll } from "../util/regex"; import { TreeSitterQuery } from "./TreeSitterQuery"; import { validateQueryCaptures } from "./TreeSitterQuery/validateQueryCaptures"; +import type { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; +import { ContiguousScopeHandler } from "../processTargets/modifiers/scopeHandlers/ContiguousScopeHandler"; /** * Represents a language definition for a single language, including the @@ -70,12 +72,21 @@ export class LanguageDefinition { * undefined if the given scope type / language id combination is still using * legacy pathways */ - getScopeHandler(scopeType: ScopeType) { + getScopeHandler(scopeType: ScopeType): ScopeHandler | undefined { if (!this.query.captureNames.includes(scopeType.type)) { return undefined; } - return new TreeSitterScopeHandler(this.query, scopeType as SimpleScopeType); + const scopeHandler = new TreeSitterScopeHandler( + this.query, + scopeType as SimpleScopeType, + ); + + if (useContiguousScopeHandler(scopeType)) { + return new ContiguousScopeHandler(scopeHandler); + } + + return scopeHandler; } } @@ -168,6 +179,18 @@ async function readQueryFileAndImports( return Object.values(rawQueryStrings).join("\n"); } +/** + * Returns true if the given scope type should use a contiguous scope handler. + */ +function useContiguousScopeHandler(scopeType: ScopeType): boolean { + switch (scopeType.type) { + case "comment": + return true; + default: + return false; + } +} + function validateImportSyntax( file: string, relativeImportPath: string, diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts index f92fde3098..ad9d09b3da 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts @@ -36,6 +36,9 @@ export interface QueryCapture { * content ranges. */ readonly allowMultiple: boolean; + /** Whether this scope should expand contiguously to its siblings. */ + readonly contiguous: boolean; + /** The insertion delimiter to use if any */ readonly insertionDelimiter: string | undefined; } @@ -63,6 +66,7 @@ export interface MutableQueryCapture extends QueryCapture { readonly document: TextDocument; range: Range; allowMultiple: boolean; + contiguous: boolean; insertionDelimiter: string | undefined; } diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts index 13ea57fe3e..b02ac6576d 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts @@ -81,6 +81,7 @@ export class TreeSitterQuery { range: getNodeRange(node), insertionDelimiter: undefined, allowMultiple: false, + contiguous: false, })), }), ) @@ -113,6 +114,7 @@ export class TreeSitterQuery { .map(({ range }) => range) .reduce((accumulator, range) => range.union(accumulator)), allowMultiple: captures.some((capture) => capture.allowMultiple), + contiguous: captures.some((capture) => capture.contiguous), insertionDelimiter: captures.find( (capture) => capture.insertionDelimiter != null, )?.insertionDelimiter, diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts index 3d4afa5729..946d7dd52c 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" | "contiguous" | "insertionDelimiter" + >[]; isValid: boolean; expectedErrorMessageIds: string[]; } @@ -192,6 +195,7 @@ suite("checkCaptureStartEnd", () => { testCase.captures.map((capture) => ({ ...capture, allowMultiple: false, + contiguous: false, insertionDelimiter: undefined, })), messages, diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts index 92177ace8f..3f24854546 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts @@ -185,6 +185,18 @@ class AllowMultiple extends QueryPredicateOperator { } } +/** Indicates that it's okay for this scope to extend contiguously through its siblings of same type. */ +class Contiguous extends QueryPredicateOperator { + name = "contiguous!" as const; + schema = z.tuple([q.node]); + + run(nodeInfo: MutableQueryCapture) { + nodeInfo.contiguous = true; + + return true; + } +} + /** * A predicate operator that logs a node, for debugging. */ @@ -254,6 +266,7 @@ export const queryPredicateOperators = [ new ChildRange(), new ShrinkToMatch(), new AllowMultiple(), + new Contiguous(), new InsertionDelimiter(), new SingleOrMultilineDelimiter(), new HasMultipleChildrenOfType(), diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts index cc8e99c789..cd2152e19f 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts @@ -54,6 +54,7 @@ function fillOutCapture(capture: NameRange): MutableQueryCapture { return { ...capture, allowMultiple: false, + contiguous: false, insertionDelimiter: undefined, document: null as unknown as TextDocument, node: null as unknown as SyntaxNode, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ContiguousScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ContiguousScopeHandler.ts new file mode 100644 index 0000000000..77aa7f73a4 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ContiguousScopeHandler.ts @@ -0,0 +1,164 @@ +import { + Direction, + Position, + Range, + ScopeType, + TextEditor, + next, +} from "@cursorless/common"; +import { Target } from "../../../typings/target.types"; +import { ensureSingleTarget } from "../../../util/targetUtils"; +import { constructScopeRangeTarget } from "../constructScopeRangeTarget"; +import { BaseScopeHandler } from "./BaseScopeHandler"; +import type { TargetScope } from "./scope.types"; +import type { + CustomScopeType, + ScopeHandler, + ScopeIteratorRequirements, +} from "./scopeHandler.types"; + +export class ContiguousScopeHandler extends BaseScopeHandler { + protected readonly isHierarchical = false; + + constructor(private scopeHandler: ScopeHandler) { + super(); + } + + get scopeType(): ScopeType | undefined { + return this.scopeHandler.scopeType; + } + + get iterationScopeType(): ScopeType | CustomScopeType { + return this.scopeHandler.iterationScopeType; + } + + *generateScopeCandidates( + editor: TextEditor, + position: Position, + direction: Direction, + _hints: ScopeIteratorRequirements, + ): Iterable { + let targetRangeOpposite = next( + generateTargetRangesInDirection( + this.scopeHandler, + editor, + position, + direction === "forward" ? "backward" : "forward", + ), + ); + + const targetRangesIter = generateTargetRangesInDirection( + this.scopeHandler, + editor, + position, + direction, + ); + + for (const targetRange of targetRangesIter) { + if ( + targetRangeOpposite != null && + isAdjacent(targetRangeOpposite.proximal, targetRange.proximal) + ) { + yield combineScopes(targetRangeOpposite.distal, targetRange.distal); + targetRangeOpposite = undefined; + } else { + yield combineScopes(targetRange.proximal, targetRange.distal); + } + } + } +} + +function combineScopes(scope1: TargetScope, scope2: TargetScope): TargetScope { + if (scope1.domain.isRangeEqual(scope2.domain)) { + return scope1; + } + + return { + editor: scope1.editor, + domain: scope1.domain.union(scope2.domain), + getTargets: (isReversed) => { + return constructScopeRangeTarget(isReversed, scope1, scope2); + }, + }; +} + +function* generateTargetRangesInDirection( + scopeHandler: ScopeHandler, + editor: TextEditor, + position: Position, + direction: Direction, +): Iterable<{ proximal: TargetScope; distal: TargetScope }> { + let proximal, distal: TargetScope | undefined; + + const generator = scopeHandler.generateScopes(editor, position, direction, { + allowAdjacentScopes: true, + skipAncestorScopes: true, + }); + + for (const scope of generator) { + if (proximal == null) { + proximal = scope; + } + + if (distal != null) { + if (!isAdjacent(distal, scope)) { + yield { proximal, distal }; + proximal = scope; + } + } + + distal = scope; + } + + if (proximal != null && distal != null) { + yield { proximal, distal }; + } +} + +function isAdjacent(scope1: TargetScope, scope2: TargetScope): boolean { + if (!scope1.contiguous || !scope2.contiguous) { + return false; + } + + if (scope1.domain.isRangeEqual(scope2.domain)) { + return true; + } + + const [startTarget, endTarget] = getTargetsInDocumentOrder( + ensureSingleTarget(scope1.getTargets(false)), + ensureSingleTarget(scope2.getTargets(false)), + ); + + const leadingRange = + startTarget.getTrailingDelimiterTarget()?.contentRange ?? + startTarget.contentRange; + const trailingRange = + endTarget.getLeadingDelimiterTarget()?.contentRange ?? + endTarget.contentRange; + + if (leadingRange.intersection(trailingRange) != null) { + return true; + } + + // Non line targets are excluded if they are separated by more than one line + if ( + !startTarget.isLine && + trailingRange.start.line - leadingRange.end.line > 1 + ) { + return false; + } + + // Finally targets are excluded if there is non whitespace text between them + const rangeBetween = new Range(leadingRange.end, trailingRange.start); + const text = startTarget.editor.document.getText(rangeBetween); + return /^\s*$/.test(text); +} + +function getTargetsInDocumentOrder( + target1: Target, + target2: Target, +): [Target, Target] { + return target1.contentRange.start.isBefore(target2.contentRange.start) + ? [target1, target2] + : [target2, target1]; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts index 373f8112cd..882933d39c 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts @@ -95,4 +95,5 @@ export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler { export interface ExtendedTargetScope extends TargetScope { allowMultiple: boolean; + contiguous: boolean; } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts index 7b0a044103..8f3c11285a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts @@ -42,7 +42,7 @@ export class TreeSitterIterationScopeHandler extends BaseTreeSitterScopeHandler return undefined; } - const { range: contentRange, allowMultiple } = capture; + const { range: contentRange, allowMultiple, contiguous } = capture; const domain = getRelatedRange(match, scopeTypeType, "iteration.domain", false) ?? @@ -52,6 +52,7 @@ export class TreeSitterIterationScopeHandler extends BaseTreeSitterScopeHandler editor, domain, allowMultiple, + contiguous, getTargets: (isReversed) => [ new PlainTarget({ editor, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts index e5d054ee63..e97e49e7ce 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts @@ -48,7 +48,12 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { return undefined; } - const { range: contentRange, allowMultiple, insertionDelimiter } = capture; + const { + range: contentRange, + allowMultiple, + contiguous, + insertionDelimiter, + } = capture; const domain = getRelatedRange(match, scopeTypeType, "domain", true) ?? contentRange; @@ -87,6 +92,7 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { editor, domain, allowMultiple, + contiguous, getTargets: (isReversed) => [ new ScopeTypeTarget({ scopeTypeType, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scope.types.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scope.types.ts index 73ce68be3d..4b191dd942 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scope.types.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scope.types.ts @@ -32,6 +32,9 @@ export interface TargetScope { */ readonly domain: Range; + /** Whether this scope could expand contiguously to its siblings. */ + readonly contiguous?: boolean; + /** * The targets corresponding to this scope. Note that there will almost * always be exactly one target, but there are some exceptions, eg "tags" in diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment.yml new file mode 100644 index 0000000000..1e87859b9b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment.yml @@ -0,0 +1,25 @@ +languageId: javascript +command: + version: 6 + spokenForm: change comment + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: { type: comment } + usePrePhraseSnapshot: true +initialState: + documentContents: |- + // Hello + // World + 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/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment2.yml new file mode 100644 index 0000000000..a1eb8cf21b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment2.yml @@ -0,0 +1,25 @@ +languageId: javascript +command: + version: 6 + spokenForm: change comment + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: { type: comment } + usePrePhraseSnapshot: true +initialState: + documentContents: |- + // Hello + // World + selections: + - anchor: { line: 1, character: 8 } + active: { line: 1, character: 8 } + marks: {} +finalState: + documentContents: "" + selections: + - anchor: { line: 0, character: 0 } + active: { line: 0, character: 0 } diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment3.yml new file mode 100644 index 0000000000..cb31e88d74 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment3.yml @@ -0,0 +1,25 @@ +languageId: javascript +command: + version: 6 + spokenForm: change comment + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: { type: comment } + usePrePhraseSnapshot: true +initialState: + documentContents: |- + // Hello + // World + selections: + - anchor: { line: 1, character: 0 } + active: { line: 1, character: 0 } + marks: {} +finalState: + documentContents: "" + selections: + - anchor: { line: 0, character: 0 } + active: { line: 0, character: 0 } diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment4.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment4.yml new file mode 100644 index 0000000000..19286e007f --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment4.yml @@ -0,0 +1,29 @@ +languageId: javascript +command: + version: 6 + spokenForm: change comment + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: { type: comment } + usePrePhraseSnapshot: true +initialState: + documentContents: |- + // Hello + + // World + selections: + - anchor: { line: 0, character: 0 } + active: { line: 0, character: 0 } + marks: {} +finalState: + documentContents: |- + + + // World + selections: + - anchor: { line: 0, character: 0 } + active: { line: 0, character: 0 } diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment5.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment5.yml new file mode 100644 index 0000000000..70ed4afb60 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment5.yml @@ -0,0 +1,25 @@ +languageId: python +command: + version: 6 + spokenForm: change comment + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: { type: comment } + usePrePhraseSnapshot: true +initialState: + documentContents: |- + # Hello + # World + 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/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment6.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment6.yml new file mode 100644 index 0000000000..16007f2c08 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment6.yml @@ -0,0 +1,25 @@ +languageId: java +command: + version: 6 + spokenForm: change comment + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: { type: comment } + usePrePhraseSnapshot: true +initialState: + documentContents: |- + // Hello + // World + 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/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment7.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment7.yml new file mode 100644 index 0000000000..d6be6a8fbe --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment7.yml @@ -0,0 +1,27 @@ +languageId: java +command: + version: 6 + spokenForm: change comment + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: { type: comment } + usePrePhraseSnapshot: true +initialState: + documentContents: |- + /* Hello */ + /* Wold */ + selections: + - anchor: { line: 0, character: 0 } + active: { line: 0, character: 0 } + marks: {} +finalState: + documentContents: |- + + /* Wold */ + selections: + - anchor: { line: 0, character: 0 } + active: { line: 0, character: 0 } diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment8.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment8.yml new file mode 100644 index 0000000000..c2cfbdbb1d --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/contiguousScope/changeComment8.yml @@ -0,0 +1,27 @@ +languageId: javascript +command: + version: 6 + spokenForm: change comment + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: { type: comment } + usePrePhraseSnapshot: true +initialState: + documentContents: |- + /** Hello */ + /** World */ + selections: + - anchor: { line: 0, character: 0 } + active: { line: 0, character: 0 } + marks: {} +finalState: + documentContents: |- + + /** World */ + selections: + - anchor: { line: 0, character: 0 } + active: { line: 0, character: 0 } 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 bcccd1e545..53f1ad70d7 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopes.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopes.vscode.test.ts @@ -157,19 +157,17 @@ function getScopeType( 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(`Unknown facet '${facetId}'`); } - const { scopeType, isIteration } = - scopeSupportFacetInfos[facetId as ScopeSupportFacet]; return { - scopeType: { type: scopeType }, - isIteration: isIteration ?? false, + scopeType: { type: facetInfo.scopeType }, + isIteration: facetInfo.isIteration ?? false, }; } diff --git a/queries/java.scm b/queries/java.scm index d10721ca7a..b5e68fc837 100644 --- a/queries/java.scm +++ b/queries/java.scm @@ -66,10 +66,14 @@ ;;!! // comment ;;! ^^^^^^^^^^ -[ - (line_comment) - (block_comment) -] @comment @textFragment +( + (line_comment) @comment @textFragment + (#contiguous! @comment) +) + +;;!! /* comment */ +;;! ^^^^^^^^^^^^^ +(block_comment) @comment @textFragment ;;!! int[] values = {1, 2, 3}; ;;! ^^^^^^^^^ diff --git a/queries/javascript.core.scm b/queries/javascript.core.scm index aa959bbf72..9b35d697d0 100644 --- a/queries/javascript.core.scm +++ b/queries/javascript.core.scm @@ -433,7 +433,15 @@ ;;!! // comment ;;! ^^^^^^^^^^ -(comment) @comment +( + (comment) @comment + (#match? @comment "^//") + (#contiguous! @comment) +) +( + (comment) @comment + (#not-match? @comment "^//") +) ;;!! /\w+/ ;;! ^^^^^ diff --git a/queries/python.scm b/queries/python.scm index 4c0f412465..b2fc9195e5 100644 --- a/queries/python.scm +++ b/queries/python.scm @@ -259,7 +259,10 @@ right: (_) @value ) @_.domain -(comment) @comment @textFragment +( + (comment) @comment @textFragment + (#contiguous! @comment) +) (string (string_start) @textFragment.start.endOf diff --git a/queries/talon.scm b/queries/talon.scm index 884e7b94ff..ce796296c7 100644 --- a/queries/talon.scm +++ b/queries/talon.scm @@ -174,4 +174,7 @@ arguments: (_) @argumentOrParameter.iteration ;;!! # foo ;;! ^^^^^ -(comment) @comment +( + (comment) @comment + (#contiguous! @comment) +)