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<unknown>): 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<T>(generator: Iterable<T>): 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<QueryCapture, "allowMultiple" | "insertionDelimiter">[];
+  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<AllowMultiple> {
   }
 }
 
+/** Indicates that it's okay for this scope to extend contiguously through its siblings of same type. */
+class Contiguous extends QueryPredicateOperator<Contiguous> {
+  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<TargetScope> {
+    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)
+)