diff --git a/cursorless-talon/src/spoken_forms.json b/cursorless-talon/src/spoken_forms.json index 243d75cbb4..dd332db2ab 100644 --- a/cursorless-talon/src/spoken_forms.json +++ b/cursorless-talon/src/spoken_forms.json @@ -174,6 +174,7 @@ "token": "token", "identifier": "identifier", "line": "line", + "full line": "fullLine", "sentence": "sentence", "block": "paragraph", "file": "document", diff --git a/data/fixtures/scopes/plaintext/fullLine.scope b/data/fixtures/scopes/plaintext/fullLine.scope new file mode 100644 index 0000000000..380ee058e5 --- /dev/null +++ b/data/fixtures/scopes/plaintext/fullLine.scope @@ -0,0 +1,10 @@ + Hello world +--- + +[Content] = +[Removal] = +[Domain] = 0:0-0:17 + >-----------------< +0| Hello world + +[Insertion delimiter] = "\n" diff --git a/data/fixtures/scopes/plaintext/fullLine2.scope b/data/fixtures/scopes/plaintext/fullLine2.scope new file mode 100644 index 0000000000..a7bd13fa2f --- /dev/null +++ b/data/fixtures/scopes/plaintext/fullLine2.scope @@ -0,0 +1,96 @@ + +Hello + world + +--- + +[#1 Content] = +[#1 Domain] = 0:0-0:0 + >< +0| + +[#1 Removal] = 0:0-1:0 + > +0| +1| Hello + < + +[#1 Trailing delimiter] = 0:0-1:0 + > +0| +1| Hello + < + +[#1 Insertion delimiter] = "\n" + + +[#2 Content] = +[#2 Domain] = 1:0-1:5 + >-----< +1| Hello + +[#2 Removal] = 1:0-2:0 + >----- +1| Hello +2| world + < + +[#2 Leading delimiter] = 0:0-1:0 + > +0| +1| Hello + < + +[#2 Trailing delimiter] = 1:5-2:0 + > +1| Hello +2| world + < + +[#2 Insertion delimiter] = "\n" + + +[#3 Content] = +[#3 Domain] = 2:0-2:7 + >-------< +2| world + +[#3 Removal] = 2:0-3:0 + >------- +2| world +3| + < + +[#3 Leading delimiter] = 1:5-2:0 + > +1| Hello +2| world + < + +[#3 Trailing delimiter] = 2:7-3:0 + > +2| world +3| + < + +[#3 Insertion delimiter] = "\n" + + +[#4 Content] = +[#4 Domain] = 3:0-3:2 + >--< +3| + +[#4 Removal] = 2:7-3:2 + > +2| world +3| + --< + +[#4 Leading delimiter] = 2:7-3:0 + > +2| world +3| + < + +[#4 Insertion delimiter] = "\n" diff --git a/data/scopeSupportFacetInfos.md b/data/scopeSupportFacetInfos.md index aa92ff1fb7..c79ccfa6b2 100644 --- a/data/scopeSupportFacetInfos.md +++ b/data/scopeSupportFacetInfos.md @@ -141,6 +141,10 @@ - `fieldAccess` A field access +### fullLine + +- `fullLine` A single full line in the document. Includes leading and trailing whitespace. + ### functionCall - `functionCall` A function call @@ -196,7 +200,7 @@ ### line -- `line` A single line in the document +- `line` A single line in the document. Excludes leading and trailing whitespace. ### list diff --git a/packages/common/src/scopeSupportFacets/PlaintextScopeSupportFacetInfos.ts b/packages/common/src/scopeSupportFacets/PlaintextScopeSupportFacetInfos.ts index 5192a950a8..03d410fff9 100644 --- a/packages/common/src/scopeSupportFacets/PlaintextScopeSupportFacetInfos.ts +++ b/packages/common/src/scopeSupportFacets/PlaintextScopeSupportFacetInfos.ts @@ -24,9 +24,15 @@ export const plaintextScopeSupportFacetInfos: Record< scopeType: "identifier", }, line: { - description: "A single line in the document", + description: + "A single line in the document. Excludes leading and trailing whitespace.", scopeType: "line", }, + fullLine: { + description: + "A single full line in the document. Includes leading and trailing whitespace.", + scopeType: "fullLine", + }, sentence: { description: "A single sentence in the document", scopeType: "sentence", diff --git a/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts index 455740f6f9..0b21f3c068 100644 --- a/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts +++ b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts @@ -303,6 +303,7 @@ export type PlaintextScopeSupportFacet = | "token" | "identifier" | "line" + | "fullLine" | "sentence" | "paragraph" | "boundedParagraph" diff --git a/packages/common/src/types/command/PartialTargetDescriptor.types.ts b/packages/common/src/types/command/PartialTargetDescriptor.types.ts index cbb68b08a9..a56e14bbc4 100644 --- a/packages/common/src/types/command/PartialTargetDescriptor.types.ts +++ b/packages/common/src/types/command/PartialTargetDescriptor.types.ts @@ -193,6 +193,7 @@ export const simpleScopeTypeTypes = [ "token", "identifier", "line", + "fullLine", "sentence", "paragraph", "boundedParagraph", diff --git a/packages/cursorless-engine/src/processTargets/marks/LineNumberStage.ts b/packages/cursorless-engine/src/processTargets/marks/LineNumberStage.ts index 97c4a2f203..9803e67430 100644 --- a/packages/cursorless-engine/src/processTargets/marks/LineNumberStage.ts +++ b/packages/cursorless-engine/src/processTargets/marks/LineNumberStage.ts @@ -5,8 +5,8 @@ import type { } from "@cursorless/common"; import { ide } from "../../singletons/ide.singleton"; import type { MarkStage } from "../PipelineStages.types"; -import { createLineTarget } from "../modifiers/scopeHandlers"; import type { LineTarget } from "../targets"; +import { createLineTarget } from "../targets"; export class LineNumberStage implements MarkStage { constructor(private mark: LineNumberMark) {} @@ -21,8 +21,8 @@ export class LineNumberStage implements MarkStage { this.mark.lineNumberType, this.mark.lineNumber, ); - const contentRange = editor.document.lineAt(lineNumber).range; - return [createLineTarget(editor, false, contentRange)]; + const line = editor.document.lineAt(lineNumber); + return [createLineTarget(editor, false, line)]; } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index da529958c5..eae2fd0536 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -4,20 +4,25 @@ import type { ScopeType, TextEditor, } from "@cursorless/common"; -import { Range } from "@cursorless/common"; -import { LineTarget } from "../../targets"; +import { createLineTarget } from "../../targets"; import { BaseScopeHandler } from "./BaseScopeHandler"; import type { TargetScope } from "./scope.types"; +interface LineScopeType { + type: "line" | "fullLine"; +} + export class LineScopeHandler extends BaseScopeHandler { - public readonly scopeType = { type: "line" } as const; public readonly iterationScopeType: ScopeType = { type: "paragraph", } as const; protected readonly isHierarchical = false; public readonly includeAdjacentInEvery = true; - constructor(_scopeType: ScopeType, _languageId: string) { + constructor( + public readonly scopeType: LineScopeType, + _languageId: string, + ) { super(); } @@ -26,13 +31,15 @@ export class LineScopeHandler extends BaseScopeHandler { position: Position, direction: Direction, ): Iterable { + const useFullLine = this.scopeType.type === "fullLine"; + if (direction === "forward") { for (let i = position.line; i < editor.document.lineCount; i++) { - yield lineNumberToScope(editor, i); + yield lineNumberToScope(editor, useFullLine, i); } } else { for (let i = position.line; i >= 0; i--) { - yield lineNumberToScope(editor, i); + yield lineNumberToScope(editor, useFullLine, i); } } } @@ -40,34 +47,16 @@ export class LineScopeHandler extends BaseScopeHandler { function lineNumberToScope( editor: TextEditor, + useFullLine: boolean, lineNumber: number, ): TargetScope { - const { range } = editor.document.lineAt(lineNumber); + const line = editor.document.lineAt(lineNumber); return { editor, - domain: range, - getTargets: (isReversed) => [createLineTarget(editor, isReversed, range)], + domain: line.range, + getTargets: (isReversed) => [ + createLineTarget(editor, isReversed, line, useFullLine), + ], }; } - -export function createLineTarget( - editor: TextEditor, - isReversed: boolean, - range: Range, -) { - return new LineTarget({ - editor, - isReversed, - contentRange: fitRangeToLineContent(editor, range), - }); -} - -export function fitRangeToLineContent(editor: TextEditor, range: Range) { - const startLine = editor.document.lineAt(range.start); - const endLine = editor.document.lineAt(range.end); - return new Range( - startLine.rangeTrimmed?.start ?? startLine.range.start, - endLine.rangeTrimmed?.end ?? endLine.range.end, - ); -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ParagraphScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ParagraphScopeHandler.ts index 54c36fd315..6da0f0a55f 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ParagraphScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ParagraphScopeHandler.ts @@ -1,15 +1,14 @@ import type { Direction, Position, - Range, ScopeType, TextDocument, TextEditor, TextLine, } from "@cursorless/common"; +import { Range } from "@cursorless/common"; import { ParagraphTarget } from "../../targets"; import { BaseScopeHandler } from "./BaseScopeHandler"; -import { fitRangeToLineContent } from "./LineScopeHandler"; import type { TargetScope } from "./scope.types"; export class ParagraphScopeHandler extends BaseScopeHandler { @@ -95,3 +94,12 @@ function createScope(editor: TextEditor, domain: Range): TargetScope { ], }; } + +function fitRangeToLineContent(editor: TextEditor, range: Range) { + const startLine = editor.document.lineAt(range.start); + const endLine = editor.document.lineAt(range.end); + return new Range( + startLine.rangeTrimmed?.start ?? startLine.range.start, + endLine.rangeTrimmed?.end ?? endLine.range.end, + ); +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts index b23314d897..e3b85c0a6a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts @@ -65,7 +65,8 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { case "identifier": return new IdentifierScopeHandler(this, scopeType, languageId); case "line": - return new LineScopeHandler(scopeType, languageId); + case "fullLine": + return new LineScopeHandler({ type: scopeType.type }, languageId); case "sentence": return new SentenceScopeHandler(this, scopeType, languageId); case "paragraph": diff --git a/packages/cursorless-engine/src/processTargets/targets/LineTarget.ts b/packages/cursorless-engine/src/processTargets/targets/LineTarget.ts index a34c8be7af..7905c887db 100644 --- a/packages/cursorless-engine/src/processTargets/targets/LineTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/LineTarget.ts @@ -1,4 +1,4 @@ -import type { TextEditor } from "@cursorless/common"; +import type { TextEditor, TextLine } from "@cursorless/common"; import { Position, Range, toLineRange } from "@cursorless/common"; import type { TextualType } from "../../typings/target.types"; import { expandToFullLine } from "../../util/rangeUtils"; @@ -94,3 +94,19 @@ export function constructLineTarget( ): LineTarget | undefined { return tryConstructTarget(LineTarget, editor, range, isReversed); } + +export function createLineTarget( + editor: TextEditor, + isReversed: boolean, + line: TextLine, + useFullRange = false, +) { + return new LineTarget({ + editor, + isReversed, + contentRange: + useFullRange || line.rangeTrimmed == null + ? line.range + : line.rangeTrimmed, + }); +} diff --git a/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts b/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts index 2569c4812e..48dc157edb 100644 --- a/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts +++ b/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts @@ -171,6 +171,7 @@ function isLanguageSpecific(scopeType: ScopeType): boolean { case "token": case "identifier": case "line": + case "fullLine": case "sentence": case "paragraph": case "boundedParagraph": diff --git a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts index 064117a802..518470e8a8 100644 --- a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts +++ b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts @@ -93,6 +93,7 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { token: "token", identifier: "identifier", line: "line", + fullLine: "full line", sentence: "sentence", paragraph: "block", boundedParagraph: "short block", diff --git a/packages/cursorless-org-docs/src/docs/contributing/scopes/fullLine.mdx b/packages/cursorless-org-docs/src/docs/contributing/scopes/fullLine.mdx new file mode 100644 index 0000000000..8d35c2dbf7 --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/contributing/scopes/fullLine.mdx @@ -0,0 +1,5 @@ +import { Scopes } from "./components/Scopes"; + +# Full line + +