From 846bd80d29bd17ca8ba388e8efae1cf0a07ff0ab Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 11 Oct 2022 17:20:34 +0100 Subject: [PATCH 01/69] Original scope handlers --- .../scopeHandlers/scopeHandler.types.ts | 103 ++++++++++++++++++ .../scopeTypeStages/ContainingScopeStage.ts | 33 ++++++ .../scopeTypeStages/EveryScopeStage.ts | 29 +++++ 3 files changed, 165 insertions(+) create mode 100644 src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts create mode 100644 src/processTargets/modifiers/scopeTypeStages/ContainingScopeStage.ts create mode 100644 src/processTargets/modifiers/scopeTypeStages/EveryScopeStage.ts diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts new file mode 100644 index 0000000000..e112764684 --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -0,0 +1,103 @@ +import { Position, Range, TextEditor } from "vscode"; +import { Target } from "../../../typings/target.types"; + +/** + * Represents a scope, which is a specific instantiation of a scope type, + * eg a specific function, or a specific line or range of lines. Contains + * {@link target}, which represents the actual scope, as well as {@link domain}, + * which represents the range within which the given scope is canonical. For + * example, a scope representing the type of a parameter will have the entire + * parameter as its domain, so that one can say "take type" from anywhere + * within the parameter. + */ +export interface Scope { + /** + * The domain within which this scope is considered the canonical instance of + * this scope type. For example, if the scope type represents a `key` in a + * key-value pair, then the pair would be the `domain`, so that "take key" + * works from anywhere within the given pair. + * + * Most scopes will have a domain that is just the content range or removal + * range of the scope. + */ + domain: Range; + + /** + * The target that represents this scope. Note that the target can represent + * a contiguous range of instances of the given scope type, eg a range from + * one function to another or a line range. + */ + target: Target; +} + +/** + * Represents an iteration scope, which is a domain containing one or more + * scopes that are considered siblings for use with `"every"`. This type + * contains {@link getScopes}, which is a list of the actual scopes, as well as + * {@link domain}, which represents the range within which the given iteration + * scope is canonical. For example, an iteration scope for the scope type + * `functionOrParameter` might have a class as its domain and its targets would + * be the functions in the class. This way one can say "take every funk" from + * anywhere within the class. + */ +export interface IterationScope { + /** + * The domain within which this iteration scope is considered the canonical + * iteration scope for the given scope type. For example, if the scope type + * is function, then the domain might be a class, so that "take every funk" + * works from anywhere within the given class. + */ + domain: Range; + + /** + * The scopes in the given iteration scope. Note that each scope has its own + * domain. We make this a function so that the scopes can be returned + * lazily. + */ + getScopes(): Scope[]; +} + +/** + * Represents a scope type, containing functions that can be used to find + * specific instances of the given scope type in a document. For example, it + * has a function to find the scope containing a given position, a function to + * find every instance of the scope in a range, etc. + */ +export interface ScopeHandler { + /** + * Given a position in a document find the smallest can scope containing the + * given position. A scope is considered to contain the position even if it is + * adjacent to the position and the position is empty. If the position is + * adjacent to two scopes prefer the one to the right. If no scope contains + * the given position return null. + * @param position The position from which to expand + */ + getScopeContainingPosition( + editor: TextEditor, + position: Position + ): Scope | null; + + /** + * Returns the iteration scopes containing {@link position}, in order of + * preference. For example, if the position is inside a class, the iteration + * scope could contain a list of functions in the class. + * + * If the target has an explicit range which extends beyond the first + * iteration scope returned, then the caller will look at the next iteration + * scopes, which are expected to be increasingly large. + * + * For example, "token" prefers an iteration scope of "line", but in order to + * support things like "every token block", "token" will have a secondary + * iteration scope of the entire file. + * + * It is common to either return a single iteration scope, or to return a + * list of two iteration scopes: the preferred scope, followed by an + * iteration scope representing the entire file. Returning more than these + * two is just an optimization. + * @param position The position from which to expand + */ + getIterationScopesContainingPosition( + editor: TextEditor, + position: Position + ): IterationScope[]; +} diff --git a/src/processTargets/modifiers/scopeTypeStages/ContainingScopeStage.ts b/src/processTargets/modifiers/scopeTypeStages/ContainingScopeStage.ts new file mode 100644 index 0000000000..c81ead42de --- /dev/null +++ b/src/processTargets/modifiers/scopeTypeStages/ContainingScopeStage.ts @@ -0,0 +1,33 @@ +import { Target } from "../../../typings/target.types"; +import { + ContainingScopeModifier, + SimpleScopeType, +} from "../../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { ModifierStage } from "../../PipelineStages.types"; +import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; + +export interface SimpleContainingScopeModifier extends ContainingScopeModifier { + scopeType: SimpleScopeType; +} + +/** + * Expands from input target to the smallest containing scope for the given + * scope type. Delegates to the {@link ScopeHandler} for the actual + * scope-specific logic, eg tree-sitter. + */ +export default class ContainingScopeStage implements ModifierStage { + constructor(private modifier: SimpleContainingScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): ScopeTypeTarget[] { + // TODO: Instantiate the right scope handler based on the scope type and + // let it do most of the work. + // For now, let's only use this stage for syntax scopes, but in the future + // we'll migrate LineStage, TokenStage, etc to use this as well. + // + // Note that this function will implement the algorithm described in + // https://github.com/cursorless-dev/cursorless/pull/629#issuecomment-1136090441, + // calling `scopeHandler.getScopeContainingPosition` at the start and then + // end of the target range. + } +} diff --git a/src/processTargets/modifiers/scopeTypeStages/EveryScopeStage.ts b/src/processTargets/modifiers/scopeTypeStages/EveryScopeStage.ts new file mode 100644 index 0000000000..5ee170d8d5 --- /dev/null +++ b/src/processTargets/modifiers/scopeTypeStages/EveryScopeStage.ts @@ -0,0 +1,29 @@ +import { Target } from "../../../typings/target.types"; +import { + EveryScopeModifier, + SimpleScopeType, +} from "../../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { ModifierStage } from "../../PipelineStages.types"; +import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; + +export interface SimpleEveryScopeModifier extends EveryScopeModifier { + scopeType: SimpleScopeType; +} + +/** + * Returns a list of targets that are contained by the given input target. If + * the range is empty then first expand to the default containing scope to use + * for this scope and search there. Delegates to the {@link ScopeHandler} for + * the actual scope-specific logic, eg tree-sitter. + */ +export default class EveryScopeStage implements ModifierStage { + constructor(private modifier: SimpleEveryScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): ScopeTypeTarget[] { + // TODO: Instantiate the right scope handler based on the scope type and + // let it do most of the work. + // For now, let's only use this stage for syntax scopes, but in the future + // we'll migrate LineStage, TokenStage, etc to use this as well. + } +} From ed0d18b3e4e823f31f4ff1e3540375ccc1c0281d Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 11 Oct 2022 18:32:01 +0100 Subject: [PATCH 02/69] More stuff --- .../TokenScopeHandler.ts} | 6 ++-- .../scopeHandlers/scopeHandler.types.ts | 12 +++---- .../scopeTypeStages/ContainingScopeStage.ts | 11 ++---- .../scopeTypeStages/SubTokenStages.ts | 2 +- .../relativeScopes/clearFirstTokenRound.yml | 35 ++++++++++++++++++ .../relativeScopes/clearLastTokenRound.yml | 35 ++++++++++++++++++ .../relativeScopes/clearNextTokenRound.yml | 36 +++++++++++++++++++ .../clearPreviousTokenRound.yml | 36 +++++++++++++++++++ 8 files changed, 153 insertions(+), 20 deletions(-) rename src/processTargets/modifiers/{scopeTypeStages/TokenStage.ts => scopeHandlers/TokenScopeHandler.ts} (97%) create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearFirstTokenRound.yml create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearLastTokenRound.yml create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearNextTokenRound.yml create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTokenRound.yml diff --git a/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts similarity index 97% rename from src/processTargets/modifiers/scopeTypeStages/TokenStage.ts rename to src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index 298885f37d..a4d50b7023 100644 --- a/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -2,14 +2,14 @@ import { Range, TextEditor } from "vscode"; import { Target } from "../../../typings/target.types"; import { ContainingScopeModifier, - EveryScopeModifier, + EveryScopeModifier } from "../../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; import { getTokensInRange, PartialToken } from "../../../util/getTokensInRange"; -import { ModifierStage } from "../../PipelineStages.types"; import { TokenTarget } from "../../targets"; +import { ScopeHandler } from "./scopeHandler.types"; -export default class implements ModifierStage { +export default class TokenScopeHandler implements ScopeHandler { constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} run(context: ProcessedTargetsContext, target: Target): TokenTarget[] { diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index e112764684..2e7dc9ec6a 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -72,10 +72,6 @@ export interface ScopeHandler { * the given position return null. * @param position The position from which to expand */ - getScopeContainingPosition( - editor: TextEditor, - position: Position - ): Scope | null; /** * Returns the iteration scopes containing {@link position}, in order of @@ -96,8 +92,10 @@ export interface ScopeHandler { * two is just an optimization. * @param position The position from which to expand */ - getIterationScopesContainingPosition( + getTargetsIntersectingRange(editor: TextEditor, range: Range): Target[]; + + getTargetsInIterationScopeContainingRange( editor: TextEditor, - position: Position - ): IterationScope[]; + range: Range + ): Target[]; } diff --git a/src/processTargets/modifiers/scopeTypeStages/ContainingScopeStage.ts b/src/processTargets/modifiers/scopeTypeStages/ContainingScopeStage.ts index c81ead42de..69654fca00 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ContainingScopeStage.ts @@ -1,23 +1,16 @@ import { Target } from "../../../typings/target.types"; -import { - ContainingScopeModifier, - SimpleScopeType, -} from "../../../typings/targetDescriptor.types"; +import { ContainingScopeModifier } from "../../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; import { ModifierStage } from "../../PipelineStages.types"; import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; -export interface SimpleContainingScopeModifier extends ContainingScopeModifier { - scopeType: SimpleScopeType; -} - /** * Expands from input target to the smallest containing scope for the given * scope type. Delegates to the {@link ScopeHandler} for the actual * scope-specific logic, eg tree-sitter. */ export default class ContainingScopeStage implements ModifierStage { - constructor(private modifier: SimpleContainingScopeModifier) {} + constructor(private modifier: ContainingScopeModifier) {} run(context: ProcessedTargetsContext, target: Target): ScopeTypeTarget[] { // TODO: Instantiate the right scope handler based on the scope type and diff --git a/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts b/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts index 06be51abd1..54150595a9 100644 --- a/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts +++ b/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts @@ -11,7 +11,7 @@ import { matchAll } from "../../../util/regex"; import { ModifierStage } from "../../PipelineStages.types"; import { PlainTarget, SubTokenWordTarget } from "../../targets"; import { SUBWORD_MATCHER } from "../subToken"; -import { getTokenRangeForSelection } from "./TokenStage"; +import { getTokenRangeForSelection } from "../scopeHandlers/TokenScopeHandler"; abstract class SubTokenStage implements ModifierStage { constructor( diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearFirstTokenRound.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearFirstTokenRound.yml new file mode 100644 index 0000000000..0ab62c2bf9 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearFirstTokenRound.yml @@ -0,0 +1,35 @@ +languageId: plaintext +command: + spokenForm: clear first token round + version: 3 + targets: + - type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: token} + start: 0 + length: 1 + - type: containingScope + scopeType: {type: surroundingPair, delimiter: parentheses} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + hello ( + there + now + ) testing + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |- + hello + there + now + ) testing + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: ordinalScope, scopeType: {type: token}, start: 0, length: 1}, {type: containingScope, scopeType: {type: surroundingPair, delimiter: parentheses}}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearLastTokenRound.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearLastTokenRound.yml new file mode 100644 index 0000000000..be522a938c --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearLastTokenRound.yml @@ -0,0 +1,35 @@ +languageId: plaintext +command: + spokenForm: clear last token round + version: 3 + targets: + - type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: token} + start: -1 + length: 1 + - type: containingScope + scopeType: {type: surroundingPair, delimiter: parentheses} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + hello ( + there + now + ) testing + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |- + hello ( + there + now + testing + selections: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: ordinalScope, scopeType: {type: token}, start: -1, length: 1}, {type: containingScope, scopeType: {type: surroundingPair, delimiter: parentheses}}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearNextTokenRound.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearNextTokenRound.yml new file mode 100644 index 0000000000..2d77cf28e3 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearNextTokenRound.yml @@ -0,0 +1,36 @@ +languageId: plaintext +command: + spokenForm: clear next token round + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 1 + direction: forward + - type: containingScope + scopeType: {type: surroundingPair, delimiter: parentheses} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + hello ( + there + now + ) testing + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |- + hello ( + there + now + ) + selections: + - anchor: {line: 3, character: 2} + active: {line: 3, character: 2} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 1, direction: forward}, {type: containingScope, scopeType: {type: surroundingPair, delimiter: parentheses}}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTokenRound.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTokenRound.yml new file mode 100644 index 0000000000..03d30c3a58 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTokenRound.yml @@ -0,0 +1,36 @@ +languageId: plaintext +command: + spokenForm: clear previous token round + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 1 + direction: backward + - type: containingScope + scopeType: {type: surroundingPair, delimiter: parentheses} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + hello ( + there + now + ) testing + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |2- + ( + there + now + ) testing + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 1, direction: backward}, {type: containingScope, scopeType: {type: surroundingPair, delimiter: parentheses}}]}] From 6d4dbfe94c8073f545bfc8c5d766ba9501e48b1a Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 12 Oct 2022 07:56:24 +0200 Subject: [PATCH 03/69] Use proper token stage in sub token stage --- .../scopeHandlers/TokenScopeHandler.ts | 243 +++++------------- .../scopeHandlers/scopeHandler.types.ts | 64 ++++- .../scopeTypeStages/SubTokenStages.ts | 15 +- 3 files changed, 127 insertions(+), 195 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index 911dacab8a..be2f229fab 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -1,202 +1,79 @@ -import { Range, TextEditor } from "vscode"; +import { Position, Range, TextEditor } from "vscode"; +import { getMatcher } from "../../../core/tokenizer"; import { Target } from "../../../typings/target.types"; -import { getTokensInRange, PartialToken } from "../../../util/getTokensInRange"; +import { getTokensInRange } from "../../../util/getTokensInRange"; +import { expandToFullLine } from "../../../util/rangeUtils"; import { TokenTarget } from "../../targets"; -import { ScopeHandler } from "./scopeHandler.types"; +import { ContainedIndices, ScopeHandler } from "./scopeHandler.types"; export default class TokenScopeHandler extends ScopeHandler { - getEveryTarget( + protected getEveryTarget( editor: TextEditor, contentRange: Range, isReversed: boolean, hasExplicitRange: boolean ): Target[] { - const start = hasExplicitRange - ? contentRange.start - : editor.document.lineAt(contentRange.start).range.start; - const end = hasExplicitRange - ? contentRange.end - : editor.document.lineAt(contentRange.end).range.end; - const range = new Range(start, end); - - const targets = getTokensInRange(editor, range).map(({ range }) => - this.getTargetFromRange(editor, isReversed, range) + const tokenRanges = getTokensInRange( + editor, + expandToFullLine(editor, contentRange) + ).map(({ range }) => range); + + const filteredTokenRanges = hasExplicitRange + ? this.filterRangesByIterationScope(contentRange, tokenRanges) + : tokenRanges; + + return filteredTokenRanges.map( + (contentRange) => + new TokenTarget({ + editor, + isReversed, + contentRange, + }) ); - - return targets; - } - - private getTargetFromRange( - editor: TextEditor, - isReversed: boolean, - range: Range - ): TokenTarget { - const contentRange = getTokenRangeForSelection(editor, range); - return new TokenTarget({ - editor: editor, - isReversed: isReversed, - contentRange, - }); } -} - -// import { Range, TextEditor } from "vscode"; -// import { Target } from "../../../typings/target.types"; -// import { -// ContainingScopeModifier, -// EveryScopeModifier -// } from "../../../typings/targetDescriptor.types"; -// import { ProcessedTargetsContext } from "../../../typings/Types"; -// import { getTokensInRange, PartialToken } from "../../../util/getTokensInRange"; -// import { TokenTarget } from "../../targets"; -// import { ScopeHandler } from "./scopeHandler.types"; - -// export default class TokenScopeHandler implements ScopeHandler { -// constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} - -// run(context: ProcessedTargetsContext, target: Target): TokenTarget[] { -// if (this.modifier.type === "everyScope") { -// return this.getEveryTarget(context, target); -// } -// return [this.getSingleTarget(target)]; -// } - -// private getEveryTarget( -// context: ProcessedTargetsContext, -// target: Target -// ): TokenTarget[] { -// const { contentRange, editor } = target; -// const start = target.hasExplicitRange -// ? contentRange.start -// : editor.document.lineAt(contentRange.start).range.start; -// const end = target.hasExplicitRange -// ? contentRange.end -// : editor.document.lineAt(contentRange.end).range.end; -// const range = new Range(start, end); - -// const targets = getTokensInRange(editor, range).map(({ range }) => -// this.getTargetFromRange(target, range) -// ); - -// if (targets.length === 0) { -// throw new Error( -// `Couldn't find containing ${this.modifier.scopeType.type}` -// ); -// } - -// return targets; -// } - -// private getSingleTarget(target: Target): TokenTarget { -// return this.getTargetFromRange(target, target.contentRange); -// } -// private getTargetFromRange(target: Target, range: Range): TokenTarget { -// const contentRange = getTokenRangeForSelection(target.editor, range); -// return new TokenTarget({ -// editor: target.editor, -// isReversed: target.isReversed, -// contentRange, -// }); -// } -// } + protected getContainingIndicesForPosition( + position: Position, + targets: Target[] + ): ContainedIndices | undefined { + const mappings = targets + .map((target, index) => ({ target, index })) + .filter((mapping) => mapping.target.contentRange.contains(position)); -/** - * Given a selection returns a new range which contains the tokens - * intersecting the given selection. Uses heuristics to tie break when the - * given selection is empty and abuts 2 adjacent tokens - * @param selection Selection to operate on - * @returns Modified range - */ -export function getTokenRangeForSelection( - editor: TextEditor, - range: Range -): Range { - let tokens = getTokenIntersectionsForSelection(editor, range); - // Use single token for overlapping or adjacent range - if (range.isEmpty) { - // If multiple matches sort and take the first - tokens.sort(({ token: a }, { token: b }) => { - // First sort on alphanumeric - const aIsAlphaNum = isAlphaNum(a.text); - const bIsAlphaNum = isAlphaNum(b.text); - if (aIsAlphaNum && !bIsAlphaNum) { - return -1; - } - if (bIsAlphaNum && !aIsAlphaNum) { - return 1; - } - // Second sort on length - const lengthDiff = b.text.length - a.text.length; - if (lengthDiff !== 0) { - return lengthDiff; - } - // Lastly sort on start position in reverse. ie prefer rightmost - return b.offsets.start - a.offsets.start; - }); - tokens = tokens.slice(0, 1); - } - // Use tokens for overlapping ranges - else { - tokens = tokens.filter((token) => !token.intersection.isEmpty); - } - if (tokens.length < 1) { - throw new Error("Couldn't find token in selection"); - } - const start = tokens[0].token.range.start; - const end = tokens[tokens.length - 1].token.range.end; - return new Range(start, end); -} - -/** - * Returns tokens that intersect with the selection that may be relevant for - * expanding the selection to its containing token. - * @param selection The selection - * @returns All tokens that intersect with the selection and are on the same line as the start or endpoint of the selection - */ -export function getTokenIntersectionsForSelection( - editor: TextEditor, - range: Range -) { - const tokens = getRelevantTokens(editor, range); - - const tokenIntersections: { token: PartialToken; intersection: Range }[] = []; - - tokens.forEach((token) => { - const intersection = token.range.intersection(range); - if (intersection != null) { - tokenIntersections.push({ token, intersection }); + if (mappings.length === 0) { + return undefined; } - }); - return tokenIntersections; -} - -/** - * Given a selection, finds all tokens that we might use to expand the - * selection. Just looks at tokens on the same line as the start and end of the - * selection, because we assume that a token cannot span multiple lines. - * @param selection The selection we care about - * @returns A list of tokens that we might expand to - */ -function getRelevantTokens(editor: TextEditor, range: Range) { - const startLine = range.start.line; - const endLine = range.end.line; + if (mappings.length > 1) { + const languageId = mappings[0].target.editor.document.languageId; + const { identifierMatcher } = getMatcher(languageId); + + // If multiple matches sort and take the first + mappings.sort(({ target: a }, { target: b }) => { + const textA = a.contentText; + const textB = b.contentText; + + // First sort on identifier(alphanumeric) + const aIsAlphaNum = identifierMatcher.test(textA); + const bIsAlphaNum = identifierMatcher.test(textB); + if (aIsAlphaNum && !bIsAlphaNum) { + return -1; + } + if (bIsAlphaNum && !aIsAlphaNum) { + return 1; + } + // Second sort on length + const lengthDiff = textA.length - textB.length; + if (lengthDiff !== 0) { + return lengthDiff; + } + // Lastly sort on start position in reverse. ie prefer rightmost + return b.contentRange.start.compareTo(a.contentRange.start); + }); + } - const tokens = getTokensInRange( - editor, - editor.document.lineAt(startLine).range - ); + const index = mappings[0].index; - if (endLine !== startLine) { - tokens.push( - ...getTokensInRange(editor, editor.document.lineAt(endLine).range) - ); + return { start: index, end: index }; } - - return tokens; -} - -function isAlphaNum(text: string) { - return /^\w+$/.test(text); } diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index e61edae4a3..9e71827640 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -1,7 +1,7 @@ // import { Position, Range, TextEditor } from "vscode"; // import { Target } from "../../../typings/target.types"; -import { Range, TextEditor } from "vscode"; +import { Position, Range, TextEditor } from "vscode"; import { Target } from "../../../typings/target.types"; import { ScopeType } from "../../../typings/targetDescriptor.types"; @@ -104,12 +104,14 @@ import { ScopeType } from "../../../typings/targetDescriptor.types"; // ): Target[]; // }, +export interface ContainedIndices { + start: number; + end: number; +} + export interface IterationScope { targets: Target[]; - containingIndices?: { - start: number; - end: number; - }; + containingIndices: ContainedIndices | undefined; } export abstract class ScopeHandler { @@ -128,12 +130,62 @@ export abstract class ScopeHandler { hasExplicitRange ); + const containingIndices = contentRange.isEmpty + ? this.getContainingIndicesForPosition(contentRange.start, targets) + : this.getContainingIndicesForRange(contentRange, targets); + return { targets, + containingIndices, }; } - abstract getEveryTarget( + protected getContainingIndicesForPosition( + position: Position, + targets: Target[] + ): ContainedIndices | undefined { + const mappings = targets + .map((target, index) => ({ range: target.contentRange, index })) + .filter((mapping) => mapping.range.contains(position)); + + if (mappings.length === 0) { + return undefined; + } + + const index = mappings.at(-1)!.index; + + return { start: index, end: index }; + } + + protected getContainingIndicesForRange( + range: Range, + targets: Target[] + ): ContainedIndices | undefined { + const mappings = targets + .map((target, index) => ({ range: target.contentRange, index })) + .filter((mapping) => { + const intersection = mapping.range.intersection(range); + return intersection != null && !intersection.isEmpty; + }); + + if (mappings.length === 0) { + return undefined; + } + + return { start: mappings[0].index, end: mappings.at(-1)!.index }; + } + + protected filterRangesByIterationScope( + iterationScope: Range, + ranges: Range[] + ): Range[] { + return ranges.filter((r) => { + const intersection = r.intersection(iterationScope); + return intersection != null && !intersection.isEmpty; + }); + } + + protected abstract getEveryTarget( editor: TextEditor, contentRange: Range, isReversed: boolean, diff --git a/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts b/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts index fef9f079f9..0c5790b990 100644 --- a/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts +++ b/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts @@ -8,21 +8,24 @@ import { } from "../../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; import { MatchedText, matchText } from "../../../util/regex"; +import getModifierStage from "../../getModifierStage"; import { ModifierStage } from "../../PipelineStages.types"; import { PlainTarget, SubTokenWordTarget } from "../../targets"; -import { getTokenRangeForSelection } from "../scopeHandlers/TokenScopeHandler"; + import { subWordSplitter } from "../subToken"; abstract class SubTokenStage implements ModifierStage { constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} run(context: ProcessedTargetsContext, target: Target): Target[] { + const tokenStage = getModifierStage({ + type: "containingScope", + scopeType: { type: "token" }, + }); + const tokenTarget = tokenStage.run(context, target)[0]; const { document } = target.editor; - const tokenRange = getTokenRangeForSelection( - target.editor, - target.contentRange - ); - const text = document.getText(tokenRange); + const tokenRange = tokenTarget.contentRange; + const text = tokenTarget.contentText; const offset = document.offsetAt(tokenRange.start); const matches = this.getMatchedText(text, document.languageId); const contentRanges = matches.map( From 71078edc4db872fe93318b78085b9b25b88f36e9 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 12 Oct 2022 15:56:27 +0200 Subject: [PATCH 04/69] Work around for identifier matcher --- src/core/tokenizer.ts | 16 +++++++++++++--- .../modifiers/scopeHandlers/TokenScopeHandler.ts | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/core/tokenizer.ts b/src/core/tokenizer.ts index 58f271dd23..1af43cca2e 100644 --- a/src/core/tokenizer.ts +++ b/src/core/tokenizer.ts @@ -123,10 +123,20 @@ const tokenMatchersForLanguage: Partial> = ); export function getMatcher(languageId: string): Matcher { - return ( + const matcher = tokenMatchersForLanguage[languageId as SupportedLanguageId] ?? - defaultMatcher - ); + defaultMatcher; + + return { + ...matcher, + // TODO Why is this necessary??????????? + identifierMatcher: new RegExp(matcher.identifierMatcher), + }; + + // return ( + // tokenMatchersForLanguage[languageId as SupportedLanguageId] ?? + // defaultMatcher + // ); } export function tokenize( diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index be2f229fab..eaf8debe6a 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -63,7 +63,7 @@ export default class TokenScopeHandler extends ScopeHandler { return 1; } // Second sort on length - const lengthDiff = textA.length - textB.length; + const lengthDiff = textB.length - textA.length; if (lengthDiff !== 0) { return lengthDiff; } From 07cc996367f6bfb8a614568ab87e51eec4d1d0ce Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 12 Oct 2022 16:50:17 +0200 Subject: [PATCH 05/69] Use new scope handler in relative scope stage --- .../modifiers/RelativeScopeStage.ts | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index 4acaf39c59..162ffc1bd8 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -3,6 +3,7 @@ import { Range } from "vscode"; import { Target } from "../../typings/target.types"; import { RelativeScopeModifier } from "../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../typings/Types"; +import getScopeHandler from "../getScopeHandler"; import { ModifierStage } from "../PipelineStages.types"; import { UntypedTarget } from "../targets"; import { @@ -15,8 +16,42 @@ export class RelativeScopeStage implements ModifierStage { constructor(private modifier: RelativeScopeModifier) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - const isForward = this.modifier.direction === "forward"; + switch (this.modifier.scopeType.type) { + case "token": + return this.runNew(target); + default: + return this.runLegacy(context, target); + } + } + private runNew(target: Target): Target[] { + const scopeHandler = getScopeHandler(this.modifier.scopeType); + const iterationScope = scopeHandler.run( + target.editor, + target.contentRange, + target.isReversed, + false + ); + + const intersectingIndices = + iterationScope.containingIndices != null + ? [ + iterationScope.containingIndices.start, + iterationScope.containingIndices.end, + ] + : []; + + return this.calculateIndicesAndCreateTarget( + target, + iterationScope.targets, + intersectingIndices + ); + } + + private runLegacy( + context: ProcessedTargetsContext, + target: Target + ): Target[] { /** * A list of targets in the iteration scope for the input {@link target}. * Note that we convert {@link target} to have no explicit range so that we @@ -32,11 +67,31 @@ export class RelativeScopeStage implements ModifierStage { this.modifier.scopeType ); + const intersectingIndices = getIntersectingTargetIndices( + target.contentRange, + targets + ); + + return this.calculateIndicesAndCreateTarget( + target, + targets, + intersectingIndices + ); + } + + private calculateIndicesAndCreateTarget( + target: Target, + targets: Target[], + intersectingIndices: number[] + ): Target[] { + const isForward = this.modifier.direction === "forward"; + /** Proximal index. This is the index closest to the target content range. */ const proximalIndex = this.computeProximalIndex( target.contentRange, targets, - isForward + isForward, + intersectingIndices ); /** Index of range farther from input target */ @@ -69,15 +124,11 @@ export class RelativeScopeStage implements ModifierStage { private computeProximalIndex( inputTargetRange: Range, targets: Target[], - isForward: boolean + isForward: boolean, + intersectingIndices: number[] ) { const includeIntersectingScopes = this.modifier.offset === 0; - const intersectingIndices = getIntersectingTargetIndices( - inputTargetRange, - targets - ); - if (intersectingIndices.length === 0) { const adjacentTargetIndex = isForward ? targets.findIndex((t) => From 298d823f3d828d2ff60bbae4965c374be5e508bf Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 12 Oct 2022 16:56:39 +0200 Subject: [PATCH 06/69] Use new scope handlers in ordinal scope stage --- .../modifiers/OrdinalScopeStage.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/processTargets/modifiers/OrdinalScopeStage.ts b/src/processTargets/modifiers/OrdinalScopeStage.ts index 8ea114e82e..5b96c0de35 100644 --- a/src/processTargets/modifiers/OrdinalScopeStage.ts +++ b/src/processTargets/modifiers/OrdinalScopeStage.ts @@ -1,6 +1,7 @@ import { Target } from "../../typings/target.types"; import { OrdinalScopeModifier } from "../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../typings/Types"; +import getScopeHandler from "../getScopeHandler"; import { ModifierStage } from "../PipelineStages.types"; import { createRangeTargetFromIndices, @@ -11,12 +12,43 @@ export class OrdinalScopeStage implements ModifierStage { constructor(private modifier: OrdinalScopeModifier) {} run(context: ProcessedTargetsContext, target: Target): Target[] { + switch (this.modifier.scopeType.type) { + case "token": + return this.runNew(target); + default: + return this.runLegacy(context, target); + } + } + + private runNew(target: Target): Target[] { + const scopeHandler = getScopeHandler(this.modifier.scopeType); + const iterationScope = scopeHandler.run( + target.editor, + target.contentRange, + target.isReversed, + target.hasExplicitRange + ); + + return this.calculateIndicesAndCreateTarget(target, iterationScope.targets); + } + + private runLegacy( + context: ProcessedTargetsContext, + target: Target + ): Target[] { const targets = getEveryScopeTargets( context, target, this.modifier.scopeType ); + return this.calculateIndicesAndCreateTarget(target, targets); + } + + private calculateIndicesAndCreateTarget( + target: Target, + targets: Target[] + ): Target[] { const startIndex = this.modifier.start + (this.modifier.start < 0 ? targets.length : 0); const endIndex = startIndex + this.modifier.length - 1; From 454e158333007ffba6ff5a0eeba65e7fd77e7247 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 12 Oct 2022 17:13:49 +0200 Subject: [PATCH 07/69] Update usage of containing indices --- .../modifiers/RelativeScopeStage.ts | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index 162ffc1bd8..20f6a26053 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -6,6 +6,7 @@ import { ProcessedTargetsContext } from "../../typings/Types"; import getScopeHandler from "../getScopeHandler"; import { ModifierStage } from "../PipelineStages.types"; import { UntypedTarget } from "../targets"; +import { ContainedIndices } from "./scopeHandlers/scopeHandler.types"; import { createRangeTargetFromIndices, getEveryScopeTargets, @@ -33,18 +34,10 @@ export class RelativeScopeStage implements ModifierStage { false ); - const intersectingIndices = - iterationScope.containingIndices != null - ? [ - iterationScope.containingIndices.start, - iterationScope.containingIndices.end, - ] - : []; - return this.calculateIndicesAndCreateTarget( target, iterationScope.targets, - intersectingIndices + iterationScope.containingIndices ); } @@ -67,7 +60,7 @@ export class RelativeScopeStage implements ModifierStage { this.modifier.scopeType ); - const intersectingIndices = getIntersectingTargetIndices( + const containingIndices = getContainingIndices( target.contentRange, targets ); @@ -75,14 +68,14 @@ export class RelativeScopeStage implements ModifierStage { return this.calculateIndicesAndCreateTarget( target, targets, - intersectingIndices + containingIndices ); } private calculateIndicesAndCreateTarget( target: Target, targets: Target[], - intersectingIndices: number[] + containingIndices: ContainedIndices | undefined ): Target[] { const isForward = this.modifier.direction === "forward"; @@ -91,7 +84,7 @@ export class RelativeScopeStage implements ModifierStage { target.contentRange, targets, isForward, - intersectingIndices + containingIndices ); /** Index of range farther from input target */ @@ -125,11 +118,11 @@ export class RelativeScopeStage implements ModifierStage { inputTargetRange: Range, targets: Target[], isForward: boolean, - intersectingIndices: number[] + containingIndices: ContainedIndices | undefined ) { const includeIntersectingScopes = this.modifier.offset === 0; - if (intersectingIndices.length === 0) { + if (containingIndices == null) { const adjacentTargetIndex = isForward ? targets.findIndex((t) => t.contentRange.start.isAfter(inputTargetRange.start) @@ -158,18 +151,20 @@ export class RelativeScopeStage implements ModifierStage { // If we've made it here, then there are scopes intersecting with // {@link inputTargetRange} - const intersectingStartIndex = intersectingIndices[0]; - const intersectingEndIndex = intersectingIndices.at(-1)!; + const intersectingStartIndex = containingIndices.start; + const intersectingEndIndex = containingIndices.end; if (includeIntersectingScopes) { // Number of scopes intersecting with input target is already greater than // desired length; throw error. This occurs if user says "two funks", and // they have 3 functions selected. Not clear what to do in that case so // we throw error. - if (intersectingIndices.length > this.modifier.length) { + const intersectingLength = + intersectingEndIndex - intersectingStartIndex + 1; + if (intersectingLength > this.modifier.length) { throw new TooFewScopesError( this.modifier.length, - intersectingIndices.length, + intersectingLength, this.modifier.scopeType.type ); } @@ -202,10 +197,10 @@ class TooFewScopesError extends Error { /** Get indices of all targets in {@link targets} intersecting with * {@link inputTargetRange} */ -function getIntersectingTargetIndices( +function getContainingIndices( inputTargetRange: Range, targets: Target[] -): number[] { +): ContainedIndices | undefined { const targetsWithIntersection = targets .map((t, i) => ({ index: i, @@ -216,14 +211,25 @@ function getIntersectingTargetIndices( // Input target range is empty. Use rightmost target and accept weak // containment. if (inputTargetRange.isEmpty) { - return targetsWithIntersection.slice(-1).map((t) => t.index); + if (targetsWithIntersection.length === 0) { + return undefined; + } + const index = targetsWithIntersection.at(-1)!.index; + return { start: index, end: index }; } // Input target range is not empty. Use all targets with non empty // intersections. - return targetsWithIntersection + const targetsWithNonEmptyIntersection = targetsWithIntersection .filter((t) => !t.intersection!.isEmpty) .map((t) => t.index); + if (targetsWithNonEmptyIntersection.length === 0) { + return undefined; + } + return { + start: targetsWithNonEmptyIntersection[0], + end: targetsWithNonEmptyIntersection.at(-1)!, + }; } function createTargetWithoutExplicitRange(target: Target) { From 4c33e8a35089196f5576cae157a1c632544f81da Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 12 Oct 2022 17:22:11 +0200 Subject: [PATCH 08/69] Rename --- src/processTargets/modifiers/RelativeScopeStage.ts | 8 ++++---- .../modifiers/scopeHandlers/TokenScopeHandler.ts | 4 ++-- .../modifiers/scopeHandlers/scopeHandler.types.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index 20f6a26053..c1106633d7 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -6,7 +6,7 @@ import { ProcessedTargetsContext } from "../../typings/Types"; import getScopeHandler from "../getScopeHandler"; import { ModifierStage } from "../PipelineStages.types"; import { UntypedTarget } from "../targets"; -import { ContainedIndices } from "./scopeHandlers/scopeHandler.types"; +import { ContainingIndices } from "./scopeHandlers/scopeHandler.types"; import { createRangeTargetFromIndices, getEveryScopeTargets, @@ -75,7 +75,7 @@ export class RelativeScopeStage implements ModifierStage { private calculateIndicesAndCreateTarget( target: Target, targets: Target[], - containingIndices: ContainedIndices | undefined + containingIndices: ContainingIndices | undefined ): Target[] { const isForward = this.modifier.direction === "forward"; @@ -118,7 +118,7 @@ export class RelativeScopeStage implements ModifierStage { inputTargetRange: Range, targets: Target[], isForward: boolean, - containingIndices: ContainedIndices | undefined + containingIndices: ContainingIndices | undefined ) { const includeIntersectingScopes = this.modifier.offset === 0; @@ -200,7 +200,7 @@ class TooFewScopesError extends Error { function getContainingIndices( inputTargetRange: Range, targets: Target[] -): ContainedIndices | undefined { +): ContainingIndices | undefined { const targetsWithIntersection = targets .map((t, i) => ({ index: i, diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index eaf8debe6a..148a77a54c 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -4,7 +4,7 @@ import { Target } from "../../../typings/target.types"; import { getTokensInRange } from "../../../util/getTokensInRange"; import { expandToFullLine } from "../../../util/rangeUtils"; import { TokenTarget } from "../../targets"; -import { ContainedIndices, ScopeHandler } from "./scopeHandler.types"; +import { ContainingIndices, ScopeHandler } from "./scopeHandler.types"; export default class TokenScopeHandler extends ScopeHandler { protected getEveryTarget( @@ -35,7 +35,7 @@ export default class TokenScopeHandler extends ScopeHandler { protected getContainingIndicesForPosition( position: Position, targets: Target[] - ): ContainedIndices | undefined { + ): ContainingIndices | undefined { const mappings = targets .map((target, index) => ({ target, index })) .filter((mapping) => mapping.target.contentRange.contains(position)); diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 9e71827640..945a1a9b1e 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -104,14 +104,14 @@ import { ScopeType } from "../../../typings/targetDescriptor.types"; // ): Target[]; // }, -export interface ContainedIndices { +export interface ContainingIndices { start: number; end: number; } export interface IterationScope { targets: Target[]; - containingIndices: ContainedIndices | undefined; + containingIndices: ContainingIndices | undefined; } export abstract class ScopeHandler { @@ -143,7 +143,7 @@ export abstract class ScopeHandler { protected getContainingIndicesForPosition( position: Position, targets: Target[] - ): ContainedIndices | undefined { + ): ContainingIndices | undefined { const mappings = targets .map((target, index) => ({ range: target.contentRange, index })) .filter((mapping) => mapping.range.contains(position)); @@ -160,7 +160,7 @@ export abstract class ScopeHandler { protected getContainingIndicesForRange( range: Range, targets: Target[] - ): ContainedIndices | undefined { + ): ContainingIndices | undefined { const mappings = targets .map((target, index) => ({ range: target.contentRange, index })) .filter((mapping) => { From c8cb599edf25ccd685dd57726b0a09759332f3ec Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 12 Oct 2022 18:41:20 +0200 Subject: [PATCH 09/69] Refactored create target --- .../scopeHandlers/TokenScopeHandler.ts | 38 ++++------ .../scopeHandlers/scopeHandler.types.ts | 74 ++++++++++++------- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index 148a77a54c..35fe4e4523 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -3,35 +3,25 @@ import { getMatcher } from "../../../core/tokenizer"; import { Target } from "../../../typings/target.types"; import { getTokensInRange } from "../../../util/getTokensInRange"; import { expandToFullLine } from "../../../util/rangeUtils"; -import { TokenTarget } from "../../targets"; -import { ContainingIndices, ScopeHandler } from "./scopeHandler.types"; +import { CommonTargetParameters, TokenTarget } from "../../targets"; +import { ContainingIndices, Scope, ScopeHandler } from "./scopeHandler.types"; export default class TokenScopeHandler extends ScopeHandler { - protected getEveryTarget( - editor: TextEditor, - contentRange: Range, - isReversed: boolean, - hasExplicitRange: boolean - ): Target[] { - const tokenRanges = getTokensInRange( - editor, - expandToFullLine(editor, contentRange) - ).map(({ range }) => range); - - const filteredTokenRanges = hasExplicitRange - ? this.filterRangesByIterationScope(contentRange, tokenRanges) - : tokenRanges; - - return filteredTokenRanges.map( - (contentRange) => - new TokenTarget({ - editor, - isReversed, - contentRange, - }) + protected getEveryScope(editor: TextEditor, contentRange: Range): Scope[] { + return getTokensInRange(editor, expandToFullLine(editor, contentRange)).map( + (token) => ({ + domain: token.range, + targetParameters: { + contentRange: token.range, + }, + }) ); } + protected createTarget(parameters: object): Target { + return new TokenTarget(parameters as CommonTargetParameters); + } + protected getContainingIndicesForPosition( position: Position, targets: Target[] diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 945a1a9b1e..87b3134394 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -114,6 +114,11 @@ export interface IterationScope { containingIndices: ContainingIndices | undefined; } +export interface Scope { + domain: Range; + targetParameters: object; +} + export abstract class ScopeHandler { constructor(protected scopeType: ScopeType) {} @@ -140,24 +145,34 @@ export abstract class ScopeHandler { }; } - protected getContainingIndicesForPosition( - position: Position, - targets: Target[] - ): ContainingIndices | undefined { - const mappings = targets - .map((target, index) => ({ range: target.contentRange, index })) - .filter((mapping) => mapping.range.contains(position)); + private getEveryTarget( + editor: TextEditor, + contentRange: Range, + isReversed: boolean, + hasExplicitRange: boolean + ): Target[] { + const scopes = this.getEveryScope(editor, contentRange); - if (mappings.length === 0) { - return undefined; - } + const filteredScopes = hasExplicitRange + ? this.filterScopesByIterationScope(contentRange, scopes) + : scopes; - const index = mappings.at(-1)!.index; + return filteredScopes.map((scope) => + this.createTarget({ ...scope.targetParameters, editor, isReversed }) + ); + } - return { start: index, end: index }; + private filterScopesByIterationScope( + iterationScope: Range, + scopes: Scope[] + ): Scope[] { + return scopes.filter((scope) => { + const intersection = scope.domain.intersection(iterationScope); + return intersection != null && !intersection.isEmpty; + }); } - protected getContainingIndicesForRange( + private getContainingIndicesForRange( range: Range, targets: Target[] ): ContainingIndices | undefined { @@ -175,20 +190,27 @@ export abstract class ScopeHandler { return { start: mappings[0].index, end: mappings.at(-1)!.index }; } - protected filterRangesByIterationScope( - iterationScope: Range, - ranges: Range[] - ): Range[] { - return ranges.filter((r) => { - const intersection = r.intersection(iterationScope); - return intersection != null && !intersection.isEmpty; - }); + protected getContainingIndicesForPosition( + position: Position, + targets: Target[] + ): ContainingIndices | undefined { + const mappings = targets + .map((target, index) => ({ range: target.contentRange, index })) + .filter((mapping) => mapping.range.contains(position)); + + if (mappings.length === 0) { + return undefined; + } + + const index = mappings.at(-1)!.index; + + return { start: index, end: index }; } - protected abstract getEveryTarget( + protected abstract getEveryScope( editor: TextEditor, - contentRange: Range, - isReversed: boolean, - hasExplicitRange: boolean - ): Target[]; + contentRange: Range + ): Scope[]; + + protected abstract createTarget(parameters: object): Target; } From 756f9bb4ae84d1bd4e86803125194db186cb208c Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 12 Oct 2022 18:43:57 +0200 Subject: [PATCH 10/69] Clean up --- .../modifiers/scopeHandlers/TokenScopeHandler.ts | 4 ++-- .../modifiers/scopeHandlers/scopeHandler.types.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index 35fe4e4523..333c1bfb0d 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -18,8 +18,8 @@ export default class TokenScopeHandler extends ScopeHandler { ); } - protected createTarget(parameters: object): Target { - return new TokenTarget(parameters as CommonTargetParameters); + protected createTarget(parameters: CommonTargetParameters): Target { + return new TokenTarget(parameters); } protected getContainingIndicesForPosition( diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 87b3134394..14cf6d8a4b 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -4,6 +4,7 @@ import { Position, Range, TextEditor } from "vscode"; import { Target } from "../../../typings/target.types"; import { ScopeType } from "../../../typings/targetDescriptor.types"; +import { CommonTargetParameters } from "../../targets"; // /** // * Represents a scope, which is a specific instantiation of a scope type, @@ -114,9 +115,13 @@ export interface IterationScope { containingIndices: ContainingIndices | undefined; } +export interface TargetParameters { + contentRange: Range; +} + export interface Scope { domain: Range; - targetParameters: object; + targetParameters: TargetParameters; } export abstract class ScopeHandler { @@ -212,5 +217,5 @@ export abstract class ScopeHandler { contentRange: Range ): Scope[]; - protected abstract createTarget(parameters: object): Target; + protected abstract createTarget(parameters: CommonTargetParameters): Target; } From 7156a024656c842168cdf1c7f0d00a332023d210 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 12 Oct 2022 18:47:28 +0200 Subject: [PATCH 11/69] clean up --- .../modifiers/scopeHandlers/TokenScopeHandler.ts | 16 +++++++++------- .../scopeHandlers/scopeHandler.types.ts | 3 --- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index 333c1bfb0d..b1da8f450a 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -8,14 +8,16 @@ import { ContainingIndices, Scope, ScopeHandler } from "./scopeHandler.types"; export default class TokenScopeHandler extends ScopeHandler { protected getEveryScope(editor: TextEditor, contentRange: Range): Scope[] { - return getTokensInRange(editor, expandToFullLine(editor, contentRange)).map( - (token) => ({ - domain: token.range, - targetParameters: { - contentRange: token.range, - }, - }) + const tokens = getTokensInRange( + editor, + expandToFullLine(editor, contentRange) ); + return tokens.map((token) => ({ + domain: token.range, + targetParameters: { + contentRange: token.range, + }, + })); } protected createTarget(parameters: CommonTargetParameters): Target { diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 14cf6d8a4b..9cc5fd0c6b 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -1,6 +1,3 @@ -// import { Position, Range, TextEditor } from "vscode"; -// import { Target } from "../../../typings/target.types"; - import { Position, Range, TextEditor } from "vscode"; import { Target } from "../../../typings/target.types"; import { ScopeType } from "../../../typings/targetDescriptor.types"; From cc4d38ed6ac5cd69f748a575db791b56dcdf579c Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 17 Oct 2022 16:46:13 +0100 Subject: [PATCH 12/69] Add some tests that should pass --- .../containingScope/clearNextToken.yml | 26 +++++++++++++++++++ .../containingScope/clearNextToken2.yml | 26 +++++++++++++++++++ .../recorded/containingScope/clearToken.yml | 23 ++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearNextToken.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearNextToken2.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearToken.yml diff --git a/src/test/suite/fixtures/recorded/containingScope/clearNextToken.yml b/src/test/suite/fixtures/recorded/containingScope/clearNextToken.yml new file mode 100644 index 0000000000..792c4433fa --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearNextToken.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear next token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: .foo bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: . bar + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 1, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearNextToken2.yml b/src/test/suite/fixtures/recorded/containingScope/clearNextToken2.yml new file mode 100644 index 0000000000..0c85b420b5 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearNextToken2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear next token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: .foo bar + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: ".foo " + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 1, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearToken.yml b/src/test/suite/fixtures/recorded/containingScope/clearToken.yml new file mode 100644 index 0000000000..1ffca5f6c1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearToken.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear token + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: foo. + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: foo + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}]}] From 73bba8833f528a698a9da7067868ccd8a100ca67 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 10:58:37 +0100 Subject: [PATCH 13/69] Add a bunch more tests --- .../containingScope/clearTwoTokens.yml | 26 +++++++++++++++++++ .../containingScope/clearTwoTokens2.yml | 26 +++++++++++++++++++ .../containingScope/clearTwoTokens3.yml | 26 +++++++++++++++++++ .../containingScope/clearTwoTokens4.yml | 26 +++++++++++++++++++ .../containingScope/clearTwoTokens5.yml | 26 +++++++++++++++++++ .../containingScope/clearTwoTokens6.yml | 26 +++++++++++++++++++ .../clearTwoTokensBackward.yml | 26 +++++++++++++++++++ .../clearTwoTokensBackward2.yml | 26 +++++++++++++++++++ .../clearTwoTokensBackward3.yml | 26 +++++++++++++++++++ .../clearTwoTokensBackward4.yml | 26 +++++++++++++++++++ .../clearTwoTokensBackward5.yml | 26 +++++++++++++++++++ .../clearTwoTokensBackward6.yml | 26 +++++++++++++++++++ 12 files changed, 312 insertions(+) create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokens2.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokens3.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokens4.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokens5.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokens6.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward2.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward3.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward4.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward5.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward6.yml diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml new file mode 100644 index 0000000000..8eca8b9f64 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb. ccc + selections: + - anchor: {line: 0, character: 7} + active: {line: 0, character: 7} + marks: {} +finalState: + documentContents: aaa bbb + selections: + - anchor: {line: 0, character: 7} + active: {line: 0, character: 7} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens2.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens2.yml new file mode 100644 index 0000000000..15c6d64767 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb. ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 7} + marks: {} +finalState: + documentContents: aaa ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens3.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens3.yml new file mode 100644 index 0000000000..175fc0a231 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens3.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb. ccc + selections: + - anchor: {line: 0, character: 7} + active: {line: 0, character: 8} + marks: {} +finalState: + documentContents: aaa bbb + selections: + - anchor: {line: 0, character: 7} + active: {line: 0, character: 7} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens4.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens4.yml new file mode 100644 index 0000000000..3a1f16fdc9 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens4.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa .bbb ccc + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: aaa . + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens5.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens5.yml new file mode 100644 index 0000000000..9fa95f34cb --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens5.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa .bbb ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: aaa ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens6.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens6.yml new file mode 100644 index 0000000000..abab566105 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens6.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa .bbb ccc + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 8} + marks: {} +finalState: + documentContents: aaa . + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward.yml new file mode 100644 index 0000000000..5fb9e859b4 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens backward + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb. ccc + selections: + - anchor: {line: 0, character: 7} + active: {line: 0, character: 7} + marks: {} +finalState: + documentContents: aaa ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward2.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward2.yml new file mode 100644 index 0000000000..4767aa2345 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens backward + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: backward + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb. ccc + selections: + - anchor: {line: 0, character: 7} + active: {line: 0, character: 8} + marks: {} +finalState: + documentContents: aaa ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward3.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward3.yml new file mode 100644 index 0000000000..e15b103679 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward3.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens backward + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: backward + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb. ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 7} + marks: {} +finalState: + documentContents: . ccc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward4.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward4.yml new file mode 100644 index 0000000000..19c02334c3 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward4.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens backward + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa .bbb ccc + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: aaa ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward5.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward5.yml new file mode 100644 index 0000000000..c958630365 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward5.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens backward + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: backward + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa .bbb ccc + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 8} + marks: {} +finalState: + documentContents: aaa ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward6.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward6.yml new file mode 100644 index 0000000000..222b1c7f31 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward6.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two tokens backward + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: backward + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa .bbb ccc + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: bbb ccc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] From b72819fc0c2e75b22ba409428499a60214305401 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 17 Oct 2022 16:37:53 +0100 Subject: [PATCH 14/69] Attempt at different approach to scope handlers --- .../modifiers/ContainingScopeStage.ts | 42 +-- .../modifiers/EveryScopeStage.ts | 36 ++- .../modifiers/OrdinalScopeStage.ts | 25 -- .../modifiers/RelativeScopeStage.ts | 283 +++++------------ .../modifiers/TooFewScopesError.ts | 13 + .../modifiers/constructScopeRangeTarget.ts | 22 ++ .../modifiers/relativeScopeLegacy.ts | 204 +++++++++++++ .../scopeHandlers/BaseScopeHandler.ts | 216 +++++++++++++ .../scopeHandlers/TokenScopeHandler.ts | 8 +- .../scopeHandlers/scopeHandler.types.ts | 289 ++++++------------ src/typings/targetDescriptor.types.ts | 4 +- 11 files changed, 671 insertions(+), 471 deletions(-) create mode 100644 src/processTargets/modifiers/TooFewScopesError.ts create mode 100644 src/processTargets/modifiers/constructScopeRangeTarget.ts create mode 100644 src/processTargets/modifiers/relativeScopeLegacy.ts create mode 100644 src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index 9b8380b73f..770d2ef97f 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -7,6 +7,7 @@ import type { import type { ProcessedTargetsContext } from "../../typings/Types"; import getScopeHandler from "../getScopeHandler"; import type { ModifierStage } from "../PipelineStages.types"; +import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import ItemStage from "./ItemStage"; import BoundedNonWhitespaceSequenceStage from "./scopeTypeStages/BoundedNonWhitespaceStage"; import ContainingSyntaxScopeStage, { @@ -38,38 +39,23 @@ export class ContainingScopeStage implements ModifierStage { } private runNew(target: Target): Target[] { - const scopeHandler = getScopeHandler(this.modifier.scopeType); - const iterationScope = scopeHandler.run( - target.editor, - target.contentRange, - target.isReversed, - target.hasExplicitRange - ); + const { + isReversed, + editor, + contentRange: { start, end }, + } = target; + const { scopeType } = this.modifier; - if (iterationScope.containingIndices == null) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - const startTarget = - iterationScope.targets[iterationScope.containingIndices.start]; - const endTarget = - iterationScope.targets[iterationScope.containingIndices.end]; + const scopeHandler = getScopeHandler(scopeType); + const startScope = scopeHandler.getScopeContainingPosition(editor, start); - if ( - iterationScope.containingIndices.start === - iterationScope.containingIndices.end - ) { - return [startTarget]; + if (startScope.domain.contains(end)) { + return [startScope.getTarget(isReversed)]; } - return [ - startTarget.createContinuousRangeTarget( - target.isReversed, - endTarget, - true, - true - ), - ]; + const endScope = scopeHandler.getScopeContainingPosition(editor, end); + + return constructScopeRangeTarget(isReversed, startScope, endScope); } private runLegacy( diff --git a/src/processTargets/modifiers/EveryScopeStage.ts b/src/processTargets/modifiers/EveryScopeStage.ts index ff27666166..292ea36ebd 100644 --- a/src/processTargets/modifiers/EveryScopeStage.ts +++ b/src/processTargets/modifiers/EveryScopeStage.ts @@ -34,19 +34,37 @@ export class EveryScopeStage implements ModifierStage { } private runNew(target: Target): Target[] { - const scopeHandler = getScopeHandler(this.modifier.scopeType); - const iterationScope = scopeHandler.run( - target.editor, - target.contentRange, - target.isReversed, - target.hasExplicitRange + const { editor, isReversed, contentRange: range } = target; + const { scopeType } = this.modifier; + + const scopeHandler = getScopeHandler(scopeType); + + if (target.hasExplicitRange) { + const scopes = scopeHandler.getScopesIntersectingRange(editor, range); + + if (scopes.length === 0) { + throw new NoContainingScopeError(scopeType.type); + } + + return scopes.map((scope) => scope.getTarget(isReversed)); + } + + const { start, end } = range; + + const startScope = scopeHandler.getIterationScopeContainingPosition( + editor, + start ); - if (iterationScope.targets.length === 0) { - throw new NoContainingScopeError(this.modifier.scopeType.type); + if (!startScope.domain.contains(end)) { + // NB: This shouldn't really happen, because our weak scopes are + // generally no bigger than a token. + throw new Error( + "Canonical iteration scope domain must include entire input range" + ); } - return iterationScope.targets; + return startScope.scopes.map((scope) => scope.getTarget(isReversed)); } private runLegacy( diff --git a/src/processTargets/modifiers/OrdinalScopeStage.ts b/src/processTargets/modifiers/OrdinalScopeStage.ts index 5b96c0de35..9e80dd0427 100644 --- a/src/processTargets/modifiers/OrdinalScopeStage.ts +++ b/src/processTargets/modifiers/OrdinalScopeStage.ts @@ -1,7 +1,6 @@ import { Target } from "../../typings/target.types"; import { OrdinalScopeModifier } from "../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../typings/Types"; -import getScopeHandler from "../getScopeHandler"; import { ModifierStage } from "../PipelineStages.types"; import { createRangeTargetFromIndices, @@ -12,30 +11,6 @@ export class OrdinalScopeStage implements ModifierStage { constructor(private modifier: OrdinalScopeModifier) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - switch (this.modifier.scopeType.type) { - case "token": - return this.runNew(target); - default: - return this.runLegacy(context, target); - } - } - - private runNew(target: Target): Target[] { - const scopeHandler = getScopeHandler(this.modifier.scopeType); - const iterationScope = scopeHandler.run( - target.editor, - target.contentRange, - target.isReversed, - target.hasExplicitRange - ); - - return this.calculateIndicesAndCreateTarget(target, iterationScope.targets); - } - - private runLegacy( - context: ProcessedTargetsContext, - target: Target - ): Target[] { const targets = getEveryScopeTargets( context, target, diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index c1106633d7..8a98264f37 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -1,17 +1,14 @@ -import { findLastIndex } from "lodash"; -import { Range } from "vscode"; +import { NoContainingScopeError } from "../../errors"; import { Target } from "../../typings/target.types"; import { RelativeScopeModifier } from "../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../typings/Types"; import getScopeHandler from "../getScopeHandler"; import { ModifierStage } from "../PipelineStages.types"; -import { UntypedTarget } from "../targets"; -import { ContainingIndices } from "./scopeHandlers/scopeHandler.types"; -import { - createRangeTargetFromIndices, - getEveryScopeTargets, - OutOfRangeError, -} from "./targetSequenceUtils"; +import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import { runLegacy } from "./relativeScopeLegacy"; +import { Scope } from "./scopeHandlers/scopeHandler.types"; +import { OutOfRangeError } from "./targetSequenceUtils"; +import { TooFewScopesError } from "./TooFewScopesError"; export class RelativeScopeStage implements ModifierStage { constructor(private modifier: RelativeScopeModifier) {} @@ -21,222 +18,104 @@ export class RelativeScopeStage implements ModifierStage { case "token": return this.runNew(target); default: - return this.runLegacy(context, target); + return runLegacy(this.modifier, context, target); } } private runNew(target: Target): Target[] { - const scopeHandler = getScopeHandler(this.modifier.scopeType); - const iterationScope = scopeHandler.run( - target.editor, - target.contentRange, - target.isReversed, - false - ); - - return this.calculateIndicesAndCreateTarget( - target, - iterationScope.targets, - iterationScope.containingIndices - ); + return this.modifier.offset === 0 + ? this.handleIncludingIntersecting(target) + : this.handleNotIncludingIntersecting(target); } - private runLegacy( - context: ProcessedTargetsContext, - target: Target - ): Target[] { - /** - * A list of targets in the iteration scope for the input {@link target}. - * Note that we convert {@link target} to have no explicit range so that we - * get all targets in the iteration scope rather than just the intersecting - * targets. - * - * FIXME: In the future we should probably use a better abstraction for this, but - * that will rely on #629 - */ - const targets = getEveryScopeTargets( - context, - createTargetWithoutExplicitRange(target), - this.modifier.scopeType + private handleNotIncludingIntersecting(target: Target): Target[] { + const { + isReversed, + editor, + contentRange: { start, end }, + } = target; + const { scopeType, length, direction, offset } = this.modifier; + + const scopeHandler = getScopeHandler(scopeType); + + const proximalScope = scopeHandler.getScopeRelativeToPosition( + editor, + direction === "forward" ? end : start, + offset, + direction ); - const containingIndices = getContainingIndices( - target.contentRange, - targets - ); + if (length === 1) { + return [proximalScope.getTarget(isReversed)]; + } - return this.calculateIndicesAndCreateTarget( - target, - targets, - containingIndices + const distalScope = scopeHandler.getScopeRelativeToPosition( + editor, + direction === "forward" + ? proximalScope.domain.end + : proximalScope.domain.start, + length - 1, + direction ); + + return constructScopeRangeTarget(isReversed, proximalScope, distalScope); } - private calculateIndicesAndCreateTarget( - target: Target, - targets: Target[], - containingIndices: ContainingIndices | undefined - ): Target[] { - const isForward = this.modifier.direction === "forward"; - - /** Proximal index. This is the index closest to the target content range. */ - const proximalIndex = this.computeProximalIndex( - target.contentRange, - targets, - isForward, - containingIndices - ); + private handleIncludingIntersecting(target: Target): Target[] { + const { isReversed, editor, contentRange: range } = target; + const { start, end } = range; + const { scopeType, length: desiredScopeCount, direction } = this.modifier; - /** Index of range farther from input target */ - const distalIndex = isForward - ? proximalIndex + this.modifier.length - 1 - : proximalIndex - this.modifier.length + 1; - - const startIndex = Math.min(proximalIndex, distalIndex); - const endIndex = Math.max(proximalIndex, distalIndex); - - return [ - createRangeTargetFromIndices( - target.isReversed, - targets, - startIndex, - endIndex - ), - ]; - } + const scopeHandler = getScopeHandler(scopeType); - /** - * Compute the index of the target that will form the near end of the range. - * - * @param inputTargetRange The range of the input target to the modifier stage - * @param targets A list of all targets under consideration (eg in iteration - * scope) - * @param isForward `true` if we are handling "next", `false` if "previous" - * @returns The index into {@link targets} that will form the near end of the range. - */ - private computeProximalIndex( - inputTargetRange: Range, - targets: Target[], - isForward: boolean, - containingIndices: ContainingIndices | undefined - ) { - const includeIntersectingScopes = this.modifier.offset === 0; - - if (containingIndices == null) { - const adjacentTargetIndex = isForward - ? targets.findIndex((t) => - t.contentRange.start.isAfter(inputTargetRange.start) - ) - : findLastIndex(targets, (t) => - t.contentRange.start.isBefore(inputTargetRange.start) - ); - - if (adjacentTargetIndex === -1) { - throw new OutOfRangeError(); - } + const intersectingScopes = scopeHandler.getScopesIntersectingRange( + editor, + range + ); - // For convenience, if they ask to include intersecting indices, we just - // start with the nearest one in the correct direction. So eg if you say - // "two funks" between functions, it will take two functions to the right - // of you. - if (includeIntersectingScopes) { - return adjacentTargetIndex; - } + const intersectingScopeCount = intersectingScopes.length; - return isForward - ? adjacentTargetIndex + this.modifier.offset - 1 - : adjacentTargetIndex - this.modifier.offset + 1; + if (intersectingScopeCount === 0) { + throw new NoContainingScopeError(scopeType.type); } - // If we've made it here, then there are scopes intersecting with - // {@link inputTargetRange} - - const intersectingStartIndex = containingIndices.start; - const intersectingEndIndex = containingIndices.end; - - if (includeIntersectingScopes) { - // Number of scopes intersecting with input target is already greater than - // desired length; throw error. This occurs if user says "two funks", and - // they have 3 functions selected. Not clear what to do in that case so - // we throw error. - const intersectingLength = - intersectingEndIndex - intersectingStartIndex + 1; - if (intersectingLength > this.modifier.length) { - throw new TooFewScopesError( - this.modifier.length, - intersectingLength, - this.modifier.scopeType.type - ); - } - - // This ensures that we count intersecting scopes in "three funks", so - // that we will never get more than 3 functions. - return isForward ? intersectingStartIndex : intersectingEndIndex; + if (intersectingScopeCount > desiredScopeCount) { + throw new TooFewScopesError( + desiredScopeCount, + intersectingScopeCount, + scopeType.type + ); } - // If we are excluding the intersecting scopes, then we set 0 to be such - // that the next scope will be the first non-intersecting. - return isForward - ? intersectingEndIndex + this.modifier.offset - : intersectingStartIndex - this.modifier.offset; - } -} + const proximalScope = + direction === "forward" + ? intersectingScopes[0] + : intersectingScopes.at(-1)!; -class TooFewScopesError extends Error { - constructor( - requestedLength: number, - currentLength: number, - scopeType: string - ) { - super( - `Requested ${requestedLength} ${scopeType}s, but ${currentLength} are already selected.` - ); - this.name = "TooFewScopesError"; - } -} + let distalScope: Scope; + + if (desiredScopeCount > intersectingScopeCount) { + const extraScopesNeeded = desiredScopeCount - intersectingScopeCount; + + const scopes = scopeHandler.getScopeRelativeToPosition( + editor, + direction === "forward" ? end : start, + [extraScopesNeeded], + direction + ); -/** Get indices of all targets in {@link targets} intersecting with - * {@link inputTargetRange} */ -function getContainingIndices( - inputTargetRange: Range, - targets: Target[] -): ContainingIndices | undefined { - const targetsWithIntersection = targets - .map((t, i) => ({ - index: i, - intersection: t.contentRange.intersection(inputTargetRange), - })) - .filter((t) => t.intersection != null); - - // Input target range is empty. Use rightmost target and accept weak - // containment. - if (inputTargetRange.isEmpty) { - if (targetsWithIntersection.length === 0) { - return undefined; + if (scopes == null) { + throw new OutOfRangeError(); + } + + distalScope = scopes[0]; + } else { + distalScope = + direction === "forward" + ? intersectingScopes.at(-1)! + : intersectingScopes[0]; } - const index = targetsWithIntersection.at(-1)!.index; - return { start: index, end: index }; - } - // Input target range is not empty. Use all targets with non empty - // intersections. - const targetsWithNonEmptyIntersection = targetsWithIntersection - .filter((t) => !t.intersection!.isEmpty) - .map((t) => t.index); - if (targetsWithNonEmptyIntersection.length === 0) { - return undefined; + return constructScopeRangeTarget(isReversed, proximalScope, distalScope); } - return { - start: targetsWithNonEmptyIntersection[0], - end: targetsWithNonEmptyIntersection.at(-1)!, - }; -} - -function createTargetWithoutExplicitRange(target: Target) { - return new UntypedTarget({ - editor: target.editor, - isReversed: target.isReversed, - contentRange: target.contentRange, - hasExplicitRange: false, - }); } diff --git a/src/processTargets/modifiers/TooFewScopesError.ts b/src/processTargets/modifiers/TooFewScopesError.ts new file mode 100644 index 0000000000..b30cdfa1e1 --- /dev/null +++ b/src/processTargets/modifiers/TooFewScopesError.ts @@ -0,0 +1,13 @@ + +export class TooFewScopesError extends Error { + constructor( + requestedLength: number, + currentLength: number, + scopeType: string + ) { + super( + `Requested ${requestedLength} ${scopeType}s, but ${currentLength} are already selected.` + ); + this.name = "TooFewScopesError"; + } +} diff --git a/src/processTargets/modifiers/constructScopeRangeTarget.ts b/src/processTargets/modifiers/constructScopeRangeTarget.ts new file mode 100644 index 0000000000..2586315f24 --- /dev/null +++ b/src/processTargets/modifiers/constructScopeRangeTarget.ts @@ -0,0 +1,22 @@ +import { Target } from "../../typings/target.types"; +import { Scope } from "./scopeHandlers/BaseScopeHandler"; + +export function constructScopeRangeTarget( + isReversed: boolean, + scope1: Scope, + scope2: Scope): Target[] { + const target1 = scope1.getTarget(isReversed); + const target2 = scope2.getTarget(isReversed); + + const isScope2After = target2.contentRange.start.isAfterOrEqual( + target1.contentRange.start + ); + + const [startTarget, endTarget] = isScope2After + ? [target1, target2] + : [target2, target1]; + + return [ + startTarget.createContinuousRangeTarget(isReversed, endTarget, true, true), + ]; +} diff --git a/src/processTargets/modifiers/relativeScopeLegacy.ts b/src/processTargets/modifiers/relativeScopeLegacy.ts new file mode 100644 index 0000000000..d0768c8f36 --- /dev/null +++ b/src/processTargets/modifiers/relativeScopeLegacy.ts @@ -0,0 +1,204 @@ +import { findLastIndex } from "lodash"; +import { Range } from "vscode"; +import { Target } from "../../typings/target.types"; +import { RelativeScopeModifier } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { UntypedTarget } from "../targets"; +import { + createRangeTargetFromIndices, + getEveryScopeTargets, + OutOfRangeError, +} from "./targetSequenceUtils"; +import { TooFewScopesError } from "./TooFewScopesError"; + +interface ContainingIndices { + start: number; + end: number; +} + +export function runLegacy( + modifier: RelativeScopeModifier, + context: ProcessedTargetsContext, + target: Target +): Target[] { + /** + * A list of targets in the iteration scope for the input {@link target}. + * Note that we convert {@link target} to have no explicit range so that we + * get all targets in the iteration scope rather than just the intersecting + * targets. + * + * FIXME: In the future we should probably use a better abstraction for this, but + * that will rely on #629 + */ + const targets = getEveryScopeTargets( + context, + createTargetWithoutExplicitRange(target), + modifier.scopeType + ); + + const containingIndices = getContainingIndices(target.contentRange, targets); + + return calculateIndicesAndCreateTarget( + modifier, + target, + targets, + containingIndices + ); +} + +function calculateIndicesAndCreateTarget( + modifier: RelativeScopeModifier, + target: Target, + targets: Target[], + containingIndices: ContainingIndices | undefined +): Target[] { + const isForward = modifier.direction === "forward"; + + /** Proximal index. This is the index closest to the target content range. */ + const proximalIndex = computeProximalIndex( + modifier, + target.contentRange, + targets, + isForward, + containingIndices + ); + + /** Index of range farther from input target */ + const distalIndex = isForward + ? proximalIndex + modifier.length - 1 + : proximalIndex - modifier.length + 1; + + const startIndex = Math.min(proximalIndex, distalIndex); + const endIndex = Math.max(proximalIndex, distalIndex); + + return [ + createRangeTargetFromIndices( + target.isReversed, + targets, + startIndex, + endIndex + ), + ]; +} + +/** + * Compute the index of the target that will form the near end of the range. + * + * @param inputTargetRange The range of the input target to the modifier stage + * @param targets A list of all targets under consideration (eg in iteration + * scope) + * @param isForward `true` if we are handling "next", `false` if "previous" + * @returns The index into {@link targets} that will form the near end of the range. + */ +function computeProximalIndex( + modifier: RelativeScopeModifier, + inputTargetRange: Range, + targets: Target[], + isForward: boolean, + containingIndices: ContainingIndices | undefined +) { + const includeIntersectingScopes = modifier.offset === 0; + + if (containingIndices == null) { + const adjacentTargetIndex = isForward + ? targets.findIndex((t) => + t.contentRange.start.isAfter(inputTargetRange.start) + ) + : findLastIndex(targets, (t) => + t.contentRange.start.isBefore(inputTargetRange.start) + ); + + if (adjacentTargetIndex === -1) { + throw new OutOfRangeError(); + } + + // For convenience, if they ask to include intersecting indices, we just + // start with the nearest one in the correct direction. So eg if you say + // "two funks" between functions, it will take two functions to the right + // of you. + if (includeIntersectingScopes) { + return adjacentTargetIndex; + } + + return isForward + ? adjacentTargetIndex + modifier.offset - 1 + : adjacentTargetIndex - modifier.offset + 1; + } + + // If we've made it here, then there are scopes intersecting with + // {@link inputTargetRange} + const intersectingStartIndex = containingIndices.start; + const intersectingEndIndex = containingIndices.end; + + if (includeIntersectingScopes) { + // Number of scopes intersecting with input target is already greater than + // desired length; throw error. This occurs if user says "two funks", and + // they have 3 functions selected. Not clear what to do in that case so + // we throw error. + const intersectingLength = + intersectingEndIndex - intersectingStartIndex + 1; + if (intersectingLength > modifier.length) { + throw new TooFewScopesError( + modifier.length, + intersectingLength, + modifier.scopeType.type + ); + } + + // This ensures that we count intersecting scopes in "three funks", so + // that we will never get more than 3 functions. + return isForward ? intersectingStartIndex : intersectingEndIndex; + } + + // If we are excluding the intersecting scopes, then we set 0 to be such + // that the next scope will be the first non-intersecting. + return isForward + ? intersectingEndIndex + modifier.offset + : intersectingStartIndex - modifier.offset; +} + +/** Get indices of all targets in {@link targets} intersecting with + * {@link inputTargetRange} */ +function getContainingIndices( + inputTargetRange: Range, + targets: Target[] +): ContainingIndices | undefined { + const targetsWithIntersection = targets + .map((t, i) => ({ + index: i, + intersection: t.contentRange.intersection(inputTargetRange), + })) + .filter((t) => t.intersection != null); + + // Input target range is empty. Use rightmost target and accept weak + // containment. + if (inputTargetRange.isEmpty) { + if (targetsWithIntersection.length === 0) { + return undefined; + } + const index = targetsWithIntersection.at(-1)!.index; + return { start: index, end: index }; + } + + // Input target range is not empty. Use all targets with non empty + // intersections. + const targetsWithNonEmptyIntersection = targetsWithIntersection + .filter((t) => !t.intersection!.isEmpty) + .map((t) => t.index); + if (targetsWithNonEmptyIntersection.length === 0) { + return undefined; + } + return { + start: targetsWithNonEmptyIntersection[0], + end: targetsWithNonEmptyIntersection.at(-1)!, + }; +} + +function createTargetWithoutExplicitRange(target: Target) { + return new UntypedTarget({ + editor: target.editor, + isReversed: target.isReversed, + contentRange: target.contentRange, + hasExplicitRange: false, + }); +} diff --git a/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts new file mode 100644 index 0000000000..55300e99ed --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts @@ -0,0 +1,216 @@ +import { Position, Range, TextEditor } from "vscode"; +import { NoContainingScopeError } from "../../../errors"; +import { Target } from "../../../typings/target.types"; +import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; +import { CommonTargetParameters } from "../../targets"; +import { IterationScope, Scope, ScopeHandler } from "./scopeHandler.types"; + +export abstract class BaseScopeHandler implements ScopeHandler { + constructor(private scopeType: ScopeType) {} + + protected abstract iterateIterationRanges( + editor: TextEditor, + position: Position, + direction: Direction + ): IterableIterator; + + protected abstract getScopesInIterationRange( + editor: TextEditor, + range: Range + ): ExtendedScope[]; + + protected abstract getContainingIterationRange( + editor: TextEditor, + position: Position + ): Range; + + getScopeContainingPosition(editor: TextEditor, position: Position): Scope { + const iterationRange = this.getContainingIterationRange(editor, position); + + const scopes = this.getScopesInIterationRange(editor, iterationRange); + + const intersectingTargets = scopes.filter(({ domain }) => + domain.contains(position) + ); + + if (intersectingTargets.length === 0) { + throw new NoContainingScopeError(this.scopeType.type); + } + + if (intersectingTargets.length === 1) { + return intersectingTargets[0]; + } + + return intersectingTargets + .sort( + (a, b) => a.compareTo?.(b) ?? a.domain.start.compareTo(b.domain.start) + ) + .at(-1)!; + } + + getScopesIntersectingRange(editor: TextEditor, range: Range): Scope[] { + const startIterationRange = this.getContainingIterationRange( + editor, + range.start + ); + const iterationRange = startIterationRange.contains(range.end) + ? startIterationRange + : startIterationRange.union( + this.getContainingIterationRange(editor, range.end) + ); + + const scopes = this.getScopesInIterationRange(editor, iterationRange); + + return scopes.filter((scope) => { + const intersection = scope.domain.intersection(range); + return intersection != null && !intersection.isEmpty; + }); + } + + getIterationScopeContainingPosition( + editor: TextEditor, + position: Position + ): IterationScope { + const iterationRange = this.getContainingIterationRange(editor, position); + + const scopes = this.getScopesInIterationRange(editor, iterationRange); + + return { domain: iterationRange, scopes }; + } + + getScopeRelativeToPosition( + editor: TextEditor, + position: Position, + offset: number, + direction: Direction + ): Scope { + const containingiterationRange = this.getContainingIterationRange( + editor, + position + ); + const scopes = this.getScopesInIterationRange( + editor, + containingiterationRange + ); + const iterator = this.iterateIterationRanges(editor, position); + for (const iterationRange of iterator) { + console.log(itItem); + } + } + + run( + editor: TextEditor, + contentRange: Range, + isReversed: boolean, + hasExplicitRange: boolean + ): IterationScope { + const targets = this.getEveryTarget( + editor, + contentRange, + isReversed, + hasExplicitRange + ); + + const containingIndices = contentRange.isEmpty + ? this.getContainingIndicesForPosition(contentRange.start, targets) + : this.getContainingIndicesForRange(contentRange, targets); + + return { + targets, + containingIndices, + }; + } + + private getEveryTarget( + editor: TextEditor, + contentRange: Range, + isReversed: boolean, + hasExplicitRange: boolean + ): Target[] { + const scopes = this.getEveryScope(editor, contentRange); + + const filteredScopes = hasExplicitRange + ? this.filterScopesByIterationScope(contentRange, scopes) + : scopes; + + return filteredScopes.map((scope) => + this.createTarget({ ...scope.targetParameters, editor, isReversed }) + ); + } + + private filterScopesByIterationScope( + iterationScope: Range, + scopes: Scope[] + ): Scope[] { + return scopes.filter((scope) => { + const intersection = scope.domain.intersection(iterationScope); + return intersection != null && !intersection.isEmpty; + }); + } + + private getContainingIndicesForRange( + range: Range, + targets: Target[] + ): ContainingIndices | undefined { + const mappings = targets + .map((target, index) => ({ range: target.contentRange, index })) + .filter((mapping) => { + const intersection = mapping.range.intersection(range); + return intersection != null && !intersection.isEmpty; + }); + + if (mappings.length === 0) { + return undefined; + } + + return { start: mappings[0].index, end: mappings.at(-1)!.index }; + } + + private getContainingIndicesForPosition( + position: Position, + targets: Target[] + ): ContainingIndices | undefined { + const mappings = targets + .map((target, index) => ({ range: target.contentRange, index })) + .filter((mapping) => mapping.range.contains(position)); + + if (mappings.length === 0) { + return undefined; + } + + const index = mappings.at(-1)!.index; + + return { start: index, end: index }; + } + + protected abstract getEveryScope( + editor: TextEditor, + contentRange: Range + ): Scope[]; + + protected abstract createTarget(parameters: CommonTargetParameters): Target; +} + +export interface ExtendedScope extends Scope { + editor: TextEditor; + compareTo?(other: ExtendedScope): number; +} + +interface ContainingIndices { + start: number; + end: number; +} + +interface InternalIterationScope { + targets: Target[]; + containingIndices: ContainingIndices | undefined; +} + +interface TargetParameters { + contentRange: Range; +} + +interface InternalScope { + domain: Range; + targetParameters: TargetParameters; +} diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index b1da8f450a..67ac34fcbe 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -4,9 +4,13 @@ import { Target } from "../../../typings/target.types"; import { getTokensInRange } from "../../../util/getTokensInRange"; import { expandToFullLine } from "../../../util/rangeUtils"; import { CommonTargetParameters, TokenTarget } from "../../targets"; -import { ContainingIndices, Scope, ScopeHandler } from "./scopeHandler.types"; +import { + ContainingIndices, + Scope, + BaseScopeHandler +} from "./BaseScopeHandler"; -export default class TokenScopeHandler extends ScopeHandler { +export default class TokenScopeHandler extends BaseScopeHandler { protected getEveryScope(editor: TextEditor, contentRange: Range): Scope[] { const tokens = getTokensInRange( editor, diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 9cc5fd0c6b..afb7baa20a 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -1,218 +1,99 @@ import { Position, Range, TextEditor } from "vscode"; import { Target } from "../../../typings/target.types"; -import { ScopeType } from "../../../typings/targetDescriptor.types"; -import { CommonTargetParameters } from "../../targets"; - -// /** -// * Represents a scope, which is a specific instantiation of a scope type, -// * eg a specific function, or a specific line or range of lines. Contains -// * {@link target}, which represents the actual scope, as well as {@link domain}, -// * which represents the range within which the given scope is canonical. For -// * example, a scope representing the type of a parameter will have the entire -// * parameter as its domain, so that one can say "take type" from anywhere -// * within the parameter. -// */ -// export interface Scope { -// /** -// * The domain within which this scope is considered the canonical instance of -// * this scope type. For example, if the scope type represents a `key` in a -// * key-value pair, then the pair would be the `domain`, so that "take key" -// * works from anywhere within the given pair. -// * -// * Most scopes will have a domain that is just the content range or removal -// * range of the scope. -// */ -// domain: Range; - -// /** -// * The target that represents this scope. Note that the target can represent -// * a contiguous range of instances of the given scope type, eg a range from -// * one function to another or a line range. -// */ -// target: Target; -// } - -// /** -// * Represents an iteration scope, which is a domain containing one or more -// * scopes that are considered siblings for use with `"every"`. This type -// * contains {@link getScopes}, which is a list of the actual scopes, as well as -// * {@link domain}, which represents the range within which the given iteration -// * scope is canonical. For example, an iteration scope for the scope type -// * `functionOrParameter` might have a class as its domain and its targets would -// * be the functions in the class. This way one can say "take every funk" from -// * anywhere within the class. -// */ -// export interface IterationScope { -// /** -// * The domain within which this iteration scope is considered the canonical -// * iteration scope for the given scope type. For example, if the scope type -// * is function, then the domain might be a class, so that "take every funk" -// * works from anywhere within the given class. -// */ -// domain: Range; - -// /** -// * The scopes in the given iteration scope. Note that each scope has its own -// * domain. We make this a function so that the scopes can be returned -// * lazily. -// */ -// getScopes(): Scope[]; -// } - -// /** -// * Represents a scope type, containing functions that can be used to find -// * specific instances of the given scope type in a document. For example, it -// * has a function to find the scope containing a given position, a function to -// * find every instance of the scope in a range, etc. -// */ -// export interface ScopeHandler { -// /** -// * Given a position in a document find the smallest can scope containing the -// * given position. A scope is considered to contain the position even if it is -// * adjacent to the position and the position is empty. If the position is -// * adjacent to two scopes prefer the one to the right. If no scope contains -// * the given position return null. -// * @param position The position from which to expand -// */ - -// /** -// * Returns the iteration scopes containing {@link position}, in order of -// * preference. For example, if the position is inside a class, the iteration -// * scope could contain a list of functions in the class. -// * -// * If the target has an explicit range which extends beyond the first -// * iteration scope returned, then the caller will look at the next iteration -// * scopes, which are expected to be increasingly large. -// * -// * For example, "token" prefers an iteration scope of "line", but in order to -// * support things like "every token block", "token" will have a secondary -// * iteration scope of the entire file. -// * -// * It is common to either return a single iteration scope, or to return a -// * list of two iteration scopes: the preferred scope, followed by an -// * iteration scope representing the entire file. Returning more than these -// * two is just an optimization. -// * @param position The position from which to expand -// */ -// getTargetsIntersectingRange(editor: TextEditor, range: Range): Target[]; - -// getTargetsInIterationScopeContainingRange( -// editor: TextEditor, -// range: Range -// ): Target[]; -// }, +import { Direction } from "../../../typings/targetDescriptor.types"; + +/** + * Represents a scope, which is a specific instantiation of a scope type, + * eg a specific function, or a specific line or range of lines. Contains + * {@link target}, which represents the actual scope, as well as {@link domain}, + * which represents the range within which the given scope is canonical. For + * example, a scope representing the type of a parameter will have the entire + * parameter as its domain, so that one can say "take type" from anywhere + * within the parameter. + */ +export interface Scope { + /** + * The domain within which this scope is considered the canonical instance of + * this scope type. For example, if the scope type represents a `key` in a + * key-value pair, then the pair would be the `domain`, so that "take key" + * works from anywhere within the given pair. + * + * Most scopes will have a domain that is just the content range or removal + * range of the scope. + */ + domain: Range; -export interface ContainingIndices { - start: number; - end: number; + /** + * The target that represents this scope. Note that the target can represent + * a contiguous range of instances of the given scope type, eg a range from + * one function to another or a line range. + */ + getTarget(isReversed: boolean): Target; } +/** + * Represents an iteration scope, which is a domain containing one or more + * scopes that are considered siblings for use with `"every"`. This type + * contains {@link getScopes}, which is a list of the actual scopes, as well as + * {@link domain}, which represents the range within which the given iteration + * scope is canonical. For example, an iteration scope for the scope type + * `functionOrParameter` might have a class as its domain and its targets would + * be the functions in the class. This way one can say "take every funk" from + * anywhere within the class. + */ export interface IterationScope { - targets: Target[]; - containingIndices: ContainingIndices | undefined; -} - -export interface TargetParameters { - contentRange: Range; -} - -export interface Scope { + /** + * The domain within which this iteration scope is considered the canonical + * iteration scope for the given scope type. For example, if the scope type + * is function, then the domain might be a class, so that "take every funk" + * works from anywhere within the given class. + */ domain: Range; - targetParameters: TargetParameters; -} -export abstract class ScopeHandler { - constructor(protected scopeType: ScopeType) {} + /** + * The scopes in the given iteration scope. Note that each scope has its own + * domain. We make this a function so that the scopes can be returned + * lazily. + */ + scopes: Scope[]; +} - run( +/** + * Represents a scope type, containing functions that can be used to find + * specific instances of the given scope type in a document. For example, it + * has a function to find the scope containing a given position, a function to + * find every instance of the scope in a range, etc. + */ +export interface ScopeHandler { + /** + * Given a position in a document, find the smallest scope containing the + * given position. A scope is considered to contain the position even if it is + * adjacent to the position. If the position is adjacent to two scopes, prefer + * the one to the right. If no scope contains the given position, return + * `undefined`. + * @param position The position from which to expand + */ + getScopeContainingPosition(editor: TextEditor, position: Position): Scope; + + getScopesIntersectingRange(editor: TextEditor, range: Range): Scope[]; + + /** + * Returns the iteration scope containing {@link position}. For example, if + * the position is inside a class, the iteration scope could contain a list + * of functions in the class. + * + * @param editor The editor containing {@link position} + * @param position The position from which to expand + */ + getIterationScopeContainingPosition( editor: TextEditor, - contentRange: Range, - isReversed: boolean, - hasExplicitRange: boolean - ): IterationScope { - const targets = this.getEveryTarget( - editor, - contentRange, - isReversed, - hasExplicitRange - ); + position: Position + ): IterationScope; - const containingIndices = contentRange.isEmpty - ? this.getContainingIndicesForPosition(contentRange.start, targets) - : this.getContainingIndicesForRange(contentRange, targets); - - return { - targets, - containingIndices, - }; - } - - private getEveryTarget( + getScopeRelativeToPosition( editor: TextEditor, - contentRange: Range, - isReversed: boolean, - hasExplicitRange: boolean - ): Target[] { - const scopes = this.getEveryScope(editor, contentRange); - - const filteredScopes = hasExplicitRange - ? this.filterScopesByIterationScope(contentRange, scopes) - : scopes; - - return filteredScopes.map((scope) => - this.createTarget({ ...scope.targetParameters, editor, isReversed }) - ); - } - - private filterScopesByIterationScope( - iterationScope: Range, - scopes: Scope[] - ): Scope[] { - return scopes.filter((scope) => { - const intersection = scope.domain.intersection(iterationScope); - return intersection != null && !intersection.isEmpty; - }); - } - - private getContainingIndicesForRange( - range: Range, - targets: Target[] - ): ContainingIndices | undefined { - const mappings = targets - .map((target, index) => ({ range: target.contentRange, index })) - .filter((mapping) => { - const intersection = mapping.range.intersection(range); - return intersection != null && !intersection.isEmpty; - }); - - if (mappings.length === 0) { - return undefined; - } - - return { start: mappings[0].index, end: mappings.at(-1)!.index }; - } - - protected getContainingIndicesForPosition( position: Position, - targets: Target[] - ): ContainingIndices | undefined { - const mappings = targets - .map((target, index) => ({ range: target.contentRange, index })) - .filter((mapping) => mapping.range.contains(position)); - - if (mappings.length === 0) { - return undefined; - } - - const index = mappings.at(-1)!.index; - - return { start: index, end: index }; - } - - protected abstract getEveryScope( - editor: TextEditor, - contentRange: Range - ): Scope[]; - - protected abstract createTarget(parameters: CommonTargetParameters): Target; + offset: number, + direction: Direction + ): Scope; } diff --git a/src/typings/targetDescriptor.types.ts b/src/typings/targetDescriptor.types.ts index cc687f1f83..5d848021c5 100644 --- a/src/typings/targetDescriptor.types.ts +++ b/src/typings/targetDescriptor.types.ts @@ -198,6 +198,8 @@ export interface OrdinalScopeModifier { length: number; } +export type Direction = "forward" | "backward"; + /** * Refer to scopes by offset relative to input target, eg "next * funk" to refer to the first function after the function containing the target input. @@ -218,7 +220,7 @@ export interface RelativeScopeModifier { /** Indicates which direction both {@link offset} and {@link length} go * relative to input target */ - direction: "forward" | "backward"; + direction: Direction; } /** From 1555fe73147af4f71300fae957ca6ac3688e1ea4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:48:27 +0000 Subject: [PATCH 15/69] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/processTargets/modifiers/TooFewScopesError.ts | 1 - src/processTargets/modifiers/constructScopeRangeTarget.ts | 3 ++- .../modifiers/scopeHandlers/TokenScopeHandler.ts | 6 +----- src/typings/targetDescriptor.types.ts | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/processTargets/modifiers/TooFewScopesError.ts b/src/processTargets/modifiers/TooFewScopesError.ts index b30cdfa1e1..39c397624b 100644 --- a/src/processTargets/modifiers/TooFewScopesError.ts +++ b/src/processTargets/modifiers/TooFewScopesError.ts @@ -1,4 +1,3 @@ - export class TooFewScopesError extends Error { constructor( requestedLength: number, diff --git a/src/processTargets/modifiers/constructScopeRangeTarget.ts b/src/processTargets/modifiers/constructScopeRangeTarget.ts index 2586315f24..110e5d32af 100644 --- a/src/processTargets/modifiers/constructScopeRangeTarget.ts +++ b/src/processTargets/modifiers/constructScopeRangeTarget.ts @@ -4,7 +4,8 @@ import { Scope } from "./scopeHandlers/BaseScopeHandler"; export function constructScopeRangeTarget( isReversed: boolean, scope1: Scope, - scope2: Scope): Target[] { + scope2: Scope +): Target[] { const target1 = scope1.getTarget(isReversed); const target2 = scope2.getTarget(isReversed); diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index 67ac34fcbe..e5cbfff2bf 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -4,11 +4,7 @@ import { Target } from "../../../typings/target.types"; import { getTokensInRange } from "../../../util/getTokensInRange"; import { expandToFullLine } from "../../../util/rangeUtils"; import { CommonTargetParameters, TokenTarget } from "../../targets"; -import { - ContainingIndices, - Scope, - BaseScopeHandler -} from "./BaseScopeHandler"; +import { ContainingIndices, Scope, BaseScopeHandler } from "./BaseScopeHandler"; export default class TokenScopeHandler extends BaseScopeHandler { protected getEveryScope(editor: TextEditor, contentRange: Range): Scope[] { diff --git a/src/typings/targetDescriptor.types.ts b/src/typings/targetDescriptor.types.ts index 5d848021c5..b6287a66ff 100644 --- a/src/typings/targetDescriptor.types.ts +++ b/src/typings/targetDescriptor.types.ts @@ -198,7 +198,7 @@ export interface OrdinalScopeModifier { length: number; } -export type Direction = "forward" | "backward"; +export type Direction = "forward" | "backward"; /** * Refer to scopes by offset relative to input target, eg "next From ef0d5710cada99c5bd39651591c2e74836d0a7ad Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 15:41:53 +0100 Subject: [PATCH 16/69] Initial implementation of new idea --- src/processTargets/getScopeHandler.ts | 5 +- src/processTargets/marks/LineNumberStage.ts | 2 +- .../modifiers/ContainingScopeStage.ts | 21 +- .../modifiers/EveryScopeStage.ts | 11 +- .../modifiers/ItemStage/getIterationScope.ts | 2 +- .../modifiers/RelativeScopeStage.ts | 109 +++++---- .../modifiers/constructScopeRangeTarget.ts | 6 +- .../modifiers/getPreferredScope.ts | 39 ++++ .../scopeHandlers/BaseRegexScopeHandler.ts | 58 +++++ .../scopeHandlers/BaseScopeHandler.ts | 216 ------------------ .../scopeHandlers/LineScopeHandler.ts | 105 +++++++++ .../scopeHandlers/NestedScopeHandler.ts | 119 ++++++++++ .../scopeHandlers/TokenScopeHandler.ts | 125 +++++----- .../scopeHandlers/scopeHandler.types.ts | 107 ++++++--- .../modifiers/scopeTypeStages/LineStage.ts | 82 ------- .../scopeTypeStages/ParagraphStage.ts | 2 +- 16 files changed, 566 insertions(+), 443 deletions(-) create mode 100644 src/processTargets/modifiers/getPreferredScope.ts create mode 100644 src/processTargets/modifiers/scopeHandlers/BaseRegexScopeHandler.ts delete mode 100644 src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts create mode 100644 src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts create mode 100644 src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts delete mode 100644 src/processTargets/modifiers/scopeTypeStages/LineStage.ts diff --git a/src/processTargets/getScopeHandler.ts b/src/processTargets/getScopeHandler.ts index 2c12f9bbb5..f01222850e 100644 --- a/src/processTargets/getScopeHandler.ts +++ b/src/processTargets/getScopeHandler.ts @@ -1,11 +1,14 @@ import { ScopeType } from "../typings/targetDescriptor.types"; +import LineScopeHandler from "./modifiers/scopeHandlers/LineScopeHandler"; import { ScopeHandler } from "./modifiers/scopeHandlers/scopeHandler.types"; import TokenScopeHandler from "./modifiers/scopeHandlers/TokenScopeHandler"; export default (scopeType: ScopeType): ScopeHandler => { switch (scopeType.type) { case "token": - return new TokenScopeHandler(scopeType); + return new TokenScopeHandler(); + case "line": + return new LineScopeHandler(); default: throw Error(`Unknown scope handler ${scopeType.type}`); } diff --git a/src/processTargets/marks/LineNumberStage.ts b/src/processTargets/marks/LineNumberStage.ts index 4a1ddb178a..5f063a0e99 100644 --- a/src/processTargets/marks/LineNumberStage.ts +++ b/src/processTargets/marks/LineNumberStage.ts @@ -4,7 +4,7 @@ import type { LineNumberType, } from "../../typings/targetDescriptor.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; -import { createLineTarget } from "../modifiers/scopeTypeStages/LineStage"; +import { createLineTarget } from "../modifiers/scopeHandlers/LineScopeHandler"; import type { MarkStage } from "../PipelineStages.types"; import { LineTarget } from "../targets"; diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index 770d2ef97f..febd1e1645 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -1,4 +1,3 @@ -import { NoContainingScopeError } from "../../errors"; import type { Target } from "../../typings/target.types"; import type { ContainingScopeModifier, @@ -8,13 +7,17 @@ import type { ProcessedTargetsContext } from "../../typings/Types"; import getScopeHandler from "../getScopeHandler"; import type { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import { + getLeftScope, + getPreferredScope, + getRightScope, +} from "./getPreferredScope"; import ItemStage from "./ItemStage"; import BoundedNonWhitespaceSequenceStage from "./scopeTypeStages/BoundedNonWhitespaceStage"; import ContainingSyntaxScopeStage, { SimpleContainingScopeModifier, } from "./scopeTypeStages/ContainingSyntaxScopeStage"; import DocumentStage from "./scopeTypeStages/DocumentStage"; -import LineStage from "./scopeTypeStages/LineStage"; import NotebookCellStage from "./scopeTypeStages/NotebookCellStage"; import ParagraphStage from "./scopeTypeStages/ParagraphStage"; import { @@ -32,6 +35,7 @@ export class ContainingScopeStage implements ModifierStage { run(context: ProcessedTargetsContext, target: Target): Target[] { switch (this.modifier.scopeType.type) { case "token": + case "line": return this.runNew(target); default: return this.runLegacy(context, target); @@ -47,13 +51,20 @@ export class ContainingScopeStage implements ModifierStage { const { scopeType } = this.modifier; const scopeHandler = getScopeHandler(scopeType); - const startScope = scopeHandler.getScopeContainingPosition(editor, start); + const startScopes = scopeHandler.getScopesContainingPosition(editor, start); + + if (end.isEqual(start)) { + return [getPreferredScope(startScopes).getTarget(isReversed)]; + } + + const startScope = getRightScope(startScopes); if (startScope.domain.contains(end)) { return [startScope.getTarget(isReversed)]; } - const endScope = scopeHandler.getScopeContainingPosition(editor, end); + const endScopes = scopeHandler.getScopesContainingPosition(editor, end); + const endScope = getLeftScope(endScopes); return constructScopeRangeTarget(isReversed, startScope, endScope); } @@ -75,8 +86,6 @@ const getContainingScopeStage = ( return new NotebookCellStage(modifier); case "document": return new DocumentStage(modifier); - case "line": - return new LineStage(modifier); case "paragraph": return new ParagraphStage(modifier); case "nonWhitespaceSequence": diff --git a/src/processTargets/modifiers/EveryScopeStage.ts b/src/processTargets/modifiers/EveryScopeStage.ts index 292ea36ebd..9506a351c4 100644 --- a/src/processTargets/modifiers/EveryScopeStage.ts +++ b/src/processTargets/modifiers/EveryScopeStage.ts @@ -4,13 +4,13 @@ import type { EveryScopeModifier } from "../../typings/targetDescriptor.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; import getScopeHandler from "../getScopeHandler"; import type { ModifierStage } from "../PipelineStages.types"; +import { getPreferredScope, getRightScope } from "./getPreferredScope"; import ItemStage from "./ItemStage"; import BoundedNonWhitespaceSequenceStage from "./scopeTypeStages/BoundedNonWhitespaceStage"; import ContainingSyntaxScopeStage, { SimpleEveryScopeModifier, } from "./scopeTypeStages/ContainingSyntaxScopeStage"; import DocumentStage from "./scopeTypeStages/DocumentStage"; -import LineStage from "./scopeTypeStages/LineStage"; import NotebookCellStage from "./scopeTypeStages/NotebookCellStage"; import ParagraphStage from "./scopeTypeStages/ParagraphStage"; import { @@ -27,6 +27,7 @@ export class EveryScopeStage implements ModifierStage { run(context: ProcessedTargetsContext, target: Target): Target[] { switch (this.modifier.scopeType.type) { case "token": + case "line": return this.runNew(target); default: return this.runLegacy(context, target); @@ -51,11 +52,15 @@ export class EveryScopeStage implements ModifierStage { const { start, end } = range; - const startScope = scopeHandler.getIterationScopeContainingPosition( + const startScopes = scopeHandler.getIterationScopesContainingPosition( editor, start ); + const startScope = end.isEqual(start) + ? getPreferredScope(startScopes) + : getRightScope(startScopes); + if (!startScope.domain.contains(end)) { // NB: This shouldn't really happen, because our weak scopes are // generally no bigger than a token. @@ -82,8 +87,6 @@ const getEveryScopeStage = (modifier: EveryScopeModifier): ModifierStage => { return new NotebookCellStage(modifier); case "document": return new DocumentStage(modifier); - case "line": - return new LineStage(modifier); case "paragraph": return new ParagraphStage(modifier); case "nonWhitespaceSequence": diff --git a/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/src/processTargets/modifiers/ItemStage/getIterationScope.ts index a01fd33075..4c728cb36f 100644 --- a/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ b/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -1,7 +1,7 @@ import { Range, TextEditor } from "vscode"; import { Target } from "../../../typings/target.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; -import { fitRangeToLineContent } from "../scopeTypeStages/LineStage"; +import { fitRangeToLineContent } from "../scopeHandlers/LineScopeHandler"; import { processSurroundingPair } from "../surroundingPair"; import { SurroundingPairInfo } from "../surroundingPair/extractSelectionFromSurroundingPairOffsets"; diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index 8a98264f37..f9c16458bd 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -1,3 +1,4 @@ +import { Position, Range, TextEditor } from "vscode"; import { NoContainingScopeError } from "../../errors"; import { Target } from "../../typings/target.types"; import { RelativeScopeModifier } from "../../typings/targetDescriptor.types"; @@ -5,9 +6,9 @@ import { ProcessedTargetsContext } from "../../typings/Types"; import getScopeHandler from "../getScopeHandler"; import { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import { getPreferredScope } from "./getPreferredScope"; import { runLegacy } from "./relativeScopeLegacy"; -import { Scope } from "./scopeHandlers/scopeHandler.types"; -import { OutOfRangeError } from "./targetSequenceUtils"; +import { ScopeHandler, TargetScope } from "./scopeHandlers/scopeHandler.types"; import { TooFewScopesError } from "./TooFewScopesError"; export class RelativeScopeStage implements ModifierStage { @@ -29,18 +30,16 @@ export class RelativeScopeStage implements ModifierStage { } private handleNotIncludingIntersecting(target: Target): Target[] { - const { - isReversed, - editor, - contentRange: { start, end }, - } = target; + const { isReversed, editor, contentRange: range } = target; const { scopeType, length, direction, offset } = this.modifier; const scopeHandler = getScopeHandler(scopeType); + const index0Scopes = getIndex0Scopes(scopeHandler, editor, range); + const proximalScope = scopeHandler.getScopeRelativeToPosition( editor, - direction === "forward" ? end : start, + getIndex0DistalPosition(direction, index0Scopes), offset, direction ); @@ -63,59 +62,81 @@ export class RelativeScopeStage implements ModifierStage { private handleIncludingIntersecting(target: Target): Target[] { const { isReversed, editor, contentRange: range } = target; - const { start, end } = range; const { scopeType, length: desiredScopeCount, direction } = this.modifier; const scopeHandler = getScopeHandler(scopeType); - const intersectingScopes = scopeHandler.getScopesIntersectingRange( - editor, - range - ); + const index0Scopes = getIndex0Scopes(scopeHandler, editor, range); - const intersectingScopeCount = intersectingScopes.length; + const index0ScopeCount = index0Scopes.length; - if (intersectingScopeCount === 0) { + if (index0ScopeCount === 0) { throw new NoContainingScopeError(scopeType.type); } - if (intersectingScopeCount > desiredScopeCount) { + if (index0ScopeCount > desiredScopeCount) { throw new TooFewScopesError( desiredScopeCount, - intersectingScopeCount, + index0ScopeCount, scopeType.type ); } const proximalScope = - direction === "forward" - ? intersectingScopes[0] - : intersectingScopes.at(-1)!; - - let distalScope: Scope; - - if (desiredScopeCount > intersectingScopeCount) { - const extraScopesNeeded = desiredScopeCount - intersectingScopeCount; - - const scopes = scopeHandler.getScopeRelativeToPosition( - editor, - direction === "forward" ? end : start, - [extraScopesNeeded], - direction - ); - - if (scopes == null) { - throw new OutOfRangeError(); - } - - distalScope = scopes[0]; - } else { - distalScope = - direction === "forward" - ? intersectingScopes.at(-1)! - : intersectingScopes[0]; - } + direction === "forward" ? index0Scopes[0] : index0Scopes.at(-1)!; + + const distalScope = + desiredScopeCount > index0ScopeCount + ? scopeHandler.getScopeRelativeToPosition( + editor, + getIndex0DistalPosition(direction, index0Scopes), + desiredScopeCount - index0ScopeCount, + direction + ) + : direction === "forward" + ? index0Scopes.at(-1)! + : index0Scopes[0]; return constructScopeRangeTarget(isReversed, proximalScope, distalScope); } } + +/** + * Returns a position that should be considered the reference position when + * finding scopes beyond index 0. + * @param direction Which direction we're going relative, eg "forward" or "backward" + * @param index0Scopes The index 0 scopes, as defined by {@link getIndex0Scopes} + * @returns The position from which indices greater than 0 should be defined + */ +function getIndex0DistalPosition( + direction: string, + index0Scopes: TargetScope[] +): Position { + return direction === "forward" + ? index0Scopes.at(-1)!.domain.end + : index0Scopes[0].domain.start; +} + +/** + * Returns a list of scopes that are considered to be at relative scope index + * 0, ie "containing" / "intersecting" with the input target. If the input + * target is zero length, we return the containing scope, otherwise we return + * the intersecting scopes. + * @param scopeHandler The scope handler for the given scope type + * @param editor The editor containing {@link range} + * @param range The input target range + * @returns The scopes that are considered to be at index 0, ie "containing" / "intersecting" with the input target + */ +function getIndex0Scopes( + scopeHandler: ScopeHandler, + editor: TextEditor, + range: Range +): TargetScope[] { + return range.isEmpty + ? [ + getPreferredScope( + scopeHandler.getScopesContainingPosition(editor, range.start) + ), + ] + : scopeHandler.getScopesIntersectingRange(editor, range); +} diff --git a/src/processTargets/modifiers/constructScopeRangeTarget.ts b/src/processTargets/modifiers/constructScopeRangeTarget.ts index 110e5d32af..786c62dd91 100644 --- a/src/processTargets/modifiers/constructScopeRangeTarget.ts +++ b/src/processTargets/modifiers/constructScopeRangeTarget.ts @@ -1,10 +1,10 @@ import { Target } from "../../typings/target.types"; -import { Scope } from "./scopeHandlers/BaseScopeHandler"; +import { TargetScope } from "./scopeHandlers/scopeHandler.types"; export function constructScopeRangeTarget( isReversed: boolean, - scope1: Scope, - scope2: Scope + scope1: TargetScope, + scope2: TargetScope ): Target[] { const target1 = scope1.getTarget(isReversed); const target2 = scope2.getTarget(isReversed); diff --git a/src/processTargets/modifiers/getPreferredScope.ts b/src/processTargets/modifiers/getPreferredScope.ts new file mode 100644 index 0000000000..dc1611774e --- /dev/null +++ b/src/processTargets/modifiers/getPreferredScope.ts @@ -0,0 +1,39 @@ +import { Scope } from "./scopeHandlers/scopeHandler.types"; + +export function getPreferredScope(scopes: T[]): T { + return getPreferredScopeHelper( + scopes, + (scope1, scope2) => + scope1.isPreferredOver?.(scope2) ?? + scope1.domain.start.isAfter(scope2.domain.start) + ); +} + +export function getLeftScope(scopes: T[]): T { + return getPreferredScopeHelper(scopes, (scope1, scope2) => + scope1.domain.start.isBefore(scope2.domain.start) + ); +} + +export function getRightScope(scopes: T[]): T { + return getPreferredScopeHelper(scopes, (scope1, scope2) => + scope1.domain.start.isAfter(scope2.domain.start) + ); +} + +function getPreferredScopeHelper( + scopes: T[], + isScope1Preferred: (scope1: Scope, scope2: Scope) => boolean +): T { + if (scopes.length === 1) { + return scopes[0]; + } + + if (scopes.length !== 2) { + throw Error("Cannot compare more than two scopes."); + } + + const [scope1, scope2] = scopes; + + return isScope1Preferred(scope1, scope2) ? scope1 : scope2; +} diff --git a/src/processTargets/modifiers/scopeHandlers/BaseRegexScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/BaseRegexScopeHandler.ts new file mode 100644 index 0000000000..5ff09596d8 --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/BaseRegexScopeHandler.ts @@ -0,0 +1,58 @@ +import { Range, TextEditor } from "vscode"; +import { Target } from "../../../typings/target.types"; +import NestedScopeHandler from "./NestedScopeHandler"; +import { ScopeHandler, TargetScope } from "./scopeHandler.types"; + +export default abstract class BaseRegexScopeHandler extends NestedScopeHandler { + constructor(parentScopeHandler: ScopeHandler) { + super(parentScopeHandler); + } + + protected abstract getRegex(editor: TextEditor, domain: Range): RegExp; + + protected abstract constructTarget( + isReversed: boolean, + editor: TextEditor, + contentRange: Range + ): Target; + + protected isPreferredOver( + _editor: TextEditor, + _scope1: TargetScope, + _scope2: TargetScope + ): boolean | undefined { + return undefined; + } + + protected getScopesInParentScope({ + editor, + domain, + }: TargetScope): TargetScope[] { + return this.getMatchesInRange(editor, domain).map((range) => { + const scope: TargetScope = { + editor, + domain: range, + getTarget: (isReversed) => + this.constructTarget(isReversed, editor, range), + }; + + scope.isPreferredOver = (other) => + this.isPreferredOver(editor, scope, other as TargetScope); + + return scope; + }); + } + + private getMatchesInRange(editor: TextEditor, range: Range): Range[] { + const offset = editor.document.offsetAt(range.start); + const text = editor.document.getText(range); + + return [...text.matchAll(this.getRegex(editor, range))].map( + (match) => + new Range( + editor.document.positionAt(offset + match.index!), + editor.document.positionAt(offset + match.index! + match[0].length) + ) + ); + } +} diff --git a/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts deleted file mode 100644 index 55300e99ed..0000000000 --- a/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { Position, Range, TextEditor } from "vscode"; -import { NoContainingScopeError } from "../../../errors"; -import { Target } from "../../../typings/target.types"; -import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; -import { CommonTargetParameters } from "../../targets"; -import { IterationScope, Scope, ScopeHandler } from "./scopeHandler.types"; - -export abstract class BaseScopeHandler implements ScopeHandler { - constructor(private scopeType: ScopeType) {} - - protected abstract iterateIterationRanges( - editor: TextEditor, - position: Position, - direction: Direction - ): IterableIterator; - - protected abstract getScopesInIterationRange( - editor: TextEditor, - range: Range - ): ExtendedScope[]; - - protected abstract getContainingIterationRange( - editor: TextEditor, - position: Position - ): Range; - - getScopeContainingPosition(editor: TextEditor, position: Position): Scope { - const iterationRange = this.getContainingIterationRange(editor, position); - - const scopes = this.getScopesInIterationRange(editor, iterationRange); - - const intersectingTargets = scopes.filter(({ domain }) => - domain.contains(position) - ); - - if (intersectingTargets.length === 0) { - throw new NoContainingScopeError(this.scopeType.type); - } - - if (intersectingTargets.length === 1) { - return intersectingTargets[0]; - } - - return intersectingTargets - .sort( - (a, b) => a.compareTo?.(b) ?? a.domain.start.compareTo(b.domain.start) - ) - .at(-1)!; - } - - getScopesIntersectingRange(editor: TextEditor, range: Range): Scope[] { - const startIterationRange = this.getContainingIterationRange( - editor, - range.start - ); - const iterationRange = startIterationRange.contains(range.end) - ? startIterationRange - : startIterationRange.union( - this.getContainingIterationRange(editor, range.end) - ); - - const scopes = this.getScopesInIterationRange(editor, iterationRange); - - return scopes.filter((scope) => { - const intersection = scope.domain.intersection(range); - return intersection != null && !intersection.isEmpty; - }); - } - - getIterationScopeContainingPosition( - editor: TextEditor, - position: Position - ): IterationScope { - const iterationRange = this.getContainingIterationRange(editor, position); - - const scopes = this.getScopesInIterationRange(editor, iterationRange); - - return { domain: iterationRange, scopes }; - } - - getScopeRelativeToPosition( - editor: TextEditor, - position: Position, - offset: number, - direction: Direction - ): Scope { - const containingiterationRange = this.getContainingIterationRange( - editor, - position - ); - const scopes = this.getScopesInIterationRange( - editor, - containingiterationRange - ); - const iterator = this.iterateIterationRanges(editor, position); - for (const iterationRange of iterator) { - console.log(itItem); - } - } - - run( - editor: TextEditor, - contentRange: Range, - isReversed: boolean, - hasExplicitRange: boolean - ): IterationScope { - const targets = this.getEveryTarget( - editor, - contentRange, - isReversed, - hasExplicitRange - ); - - const containingIndices = contentRange.isEmpty - ? this.getContainingIndicesForPosition(contentRange.start, targets) - : this.getContainingIndicesForRange(contentRange, targets); - - return { - targets, - containingIndices, - }; - } - - private getEveryTarget( - editor: TextEditor, - contentRange: Range, - isReversed: boolean, - hasExplicitRange: boolean - ): Target[] { - const scopes = this.getEveryScope(editor, contentRange); - - const filteredScopes = hasExplicitRange - ? this.filterScopesByIterationScope(contentRange, scopes) - : scopes; - - return filteredScopes.map((scope) => - this.createTarget({ ...scope.targetParameters, editor, isReversed }) - ); - } - - private filterScopesByIterationScope( - iterationScope: Range, - scopes: Scope[] - ): Scope[] { - return scopes.filter((scope) => { - const intersection = scope.domain.intersection(iterationScope); - return intersection != null && !intersection.isEmpty; - }); - } - - private getContainingIndicesForRange( - range: Range, - targets: Target[] - ): ContainingIndices | undefined { - const mappings = targets - .map((target, index) => ({ range: target.contentRange, index })) - .filter((mapping) => { - const intersection = mapping.range.intersection(range); - return intersection != null && !intersection.isEmpty; - }); - - if (mappings.length === 0) { - return undefined; - } - - return { start: mappings[0].index, end: mappings.at(-1)!.index }; - } - - private getContainingIndicesForPosition( - position: Position, - targets: Target[] - ): ContainingIndices | undefined { - const mappings = targets - .map((target, index) => ({ range: target.contentRange, index })) - .filter((mapping) => mapping.range.contains(position)); - - if (mappings.length === 0) { - return undefined; - } - - const index = mappings.at(-1)!.index; - - return { start: index, end: index }; - } - - protected abstract getEveryScope( - editor: TextEditor, - contentRange: Range - ): Scope[]; - - protected abstract createTarget(parameters: CommonTargetParameters): Target; -} - -export interface ExtendedScope extends Scope { - editor: TextEditor; - compareTo?(other: ExtendedScope): number; -} - -interface ContainingIndices { - start: number; - end: number; -} - -interface InternalIterationScope { - targets: Target[]; - containingIndices: ContainingIndices | undefined; -} - -interface TargetParameters { - contentRange: Range; -} - -interface InternalScope { - domain: Range; - targetParameters: TargetParameters; -} diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts new file mode 100644 index 0000000000..ae3f056999 --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -0,0 +1,105 @@ +import { range } from "lodash"; +import { Position, Range, TextEditor } from "vscode"; +import { ScopeType } from "../../../core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types"; +import { NoContainingScopeError } from "../../../errors"; +import { Direction } from "../../../typings/targetDescriptor.types"; +import { getDocumentRange } from "../../../util/range"; +import { LineTarget } from "../../targets"; +import { + IterationScope, + ScopeHandler, + TargetScope, +} from "./scopeHandler.types"; + +export default class LineScopeHandler implements ScopeHandler { + get scopeType(): ScopeType { + return { type: "line" }; + } + + getScopesContainingPosition( + editor: TextEditor, + position: Position + ): TargetScope[] { + return [lineNumberToScope(editor, position.line)]; + } + + getScopesIntersectingRange( + editor: TextEditor, + { start, end }: Range + ): TargetScope[] { + return range(start.line, end.line + 1).map((lineNumber) => + lineNumberToScope(editor, lineNumber) + ); + } + + getIterationScopesContainingPosition( + editor: TextEditor, + _position: Position + ): IterationScope[] { + return [ + { + editor, + domain: getDocumentRange(editor.document), + scopes: range(editor.document.lineCount).map((lineNumber) => + lineNumberToScope(editor, lineNumber) + ), + }, + ]; + } + + getScopeRelativeToPosition( + editor: TextEditor, + position: Position, + offset: number, + direction: Direction + ): TargetScope { + const lineNumber = + direction === "forward" ? position.line + offset : position.line - offset; + + if (lineNumber < 0 || lineNumber >= editor.document.lineCount) { + throw new NoContainingScopeError("line"); + } + + return lineNumberToScope(editor, lineNumber); + } +} + +function lineNumberToScope( + editor: TextEditor, + lineNumber: number +): TargetScope { + const { range } = editor.document.lineAt(lineNumber); + + return { + editor, + domain: range, + getTarget: (isReversed) => createLineTarget(editor, isReversed, range), + }; +} + +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); + const endCharacterIndex = + endLine.range.end.character - + (endLine.text.length - endLine.text.trimEnd().length); + + return new Range( + startLine.lineNumber, + startLine.firstNonWhitespaceCharacterIndex, + endLine.lineNumber, + endCharacterIndex + ); +} diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts new file mode 100644 index 0000000000..3e61ac2943 --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -0,0 +1,119 @@ +import { TextEditor, Position, Range } from "vscode"; +import { NoContainingScopeError } from "../../../errors"; +import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; +import { getPreferredScope } from "../getPreferredScope"; +import { + IterationScope, + ScopeHandler, + TargetScope, +} from "./scopeHandler.types"; + +export default abstract class NestedScopeHandler implements ScopeHandler { + constructor(private parentScopeHandler: ScopeHandler) {} + + abstract get scopeType(): ScopeType; + + protected abstract getScopesInParentScope( + parentScope: TargetScope + ): TargetScope[]; + + getScopesContainingPosition( + editor: TextEditor, + position: Position + ): TargetScope[] { + const parentScope = getPreferredScope( + this.parentScopeHandler.getScopesContainingPosition(editor, position) + ); + + return this.getScopesInParentScope(parentScope).filter(({ domain }) => + domain.contains(position) + ); + } + + getScopesIntersectingRange(editor: TextEditor, range: Range): TargetScope[] { + return this.parentScopeHandler + .getScopesIntersectingRange(editor, range) + .flatMap((parentScope) => this.getScopesInParentScope(parentScope)) + .filter(({ domain }) => { + const intersection = domain.intersection(range); + return intersection != null && !intersection.isEmpty; + }); + } + + getIterationScopesContainingPosition( + editor: TextEditor, + position: Position + ): IterationScope[] { + return this.parentScopeHandler + .getScopesContainingPosition(editor, position) + .map((parentScope) => ({ + domain: parentScope.domain, + editor, + isPreferredOver: parentScope.isPreferredOver, + scopes: this.getScopesInParentScope(parentScope), + })); + } + + getScopeRelativeToPosition( + editor: TextEditor, + position: Position, + offset: number, + direction: Direction + ): TargetScope { + let remainingOffset = offset; + + const iterator = this.iterateScopeGroups(editor, position, direction); + for (const scopes of iterator) { + if (scopes.length >= remainingOffset) { + return direction === "forward" + ? scopes.at(remainingOffset - 1)! + : scopes.at(-remainingOffset)!; + } + + remainingOffset -= scopes.length; + } + + throw new NoContainingScopeError(this.scopeType.type); + } + + private *iterateScopeGroups( + editor: TextEditor, + position: Position, + direction: Direction + ): Generator { + const containingParentScope = getPreferredScope( + this.parentScopeHandler.getScopesContainingPosition(editor, position) + ); + + yield this.getScopesInParentScope(containingParentScope).filter( + ({ domain }) => + direction === "forward" + ? domain.start.isAfterOrEqual(position) + : domain.end.isBeforeOrEqual(position) + ); + + let parentOffset = 1; + let currentPosition = + direction === "forward" + ? containingParentScope.domain.start + : containingParentScope.domain.end; + + while (true) { + const parentScope = this.parentScopeHandler.getScopeRelativeToPosition( + editor, + currentPosition, + parentOffset, + direction + ); + + yield this.getScopesInParentScope(parentScope); + + currentPosition = + direction === "forward" + ? parentScope.domain.start + : parentScope.domain.end; + + parentOffset += 1; + } + } +} diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index e5cbfff2bf..be5d30558c 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -1,71 +1,84 @@ -import { Position, Range, TextEditor } from "vscode"; +import { Range, TextEditor } from "vscode"; import { getMatcher } from "../../../core/tokenizer"; import { Target } from "../../../typings/target.types"; -import { getTokensInRange } from "../../../util/getTokensInRange"; -import { expandToFullLine } from "../../../util/rangeUtils"; -import { CommonTargetParameters, TokenTarget } from "../../targets"; -import { ContainingIndices, Scope, BaseScopeHandler } from "./BaseScopeHandler"; +import { ScopeType } from "../../../typings/targetDescriptor.types"; +import { TokenTarget } from "../../targets"; +import BaseRegexScopeHandler from "./BaseRegexScopeHandler"; +import LineScopeHandler from "./LineScopeHandler"; +import { TargetScope } from "./scopeHandler.types"; -export default class TokenScopeHandler extends BaseScopeHandler { - protected getEveryScope(editor: TextEditor, contentRange: Range): Scope[] { - const tokens = getTokensInRange( - editor, - expandToFullLine(editor, contentRange) - ); - return tokens.map((token) => ({ - domain: token.range, - targetParameters: { - contentRange: token.range, - }, - })); +export default class TokenScopeHandler extends BaseRegexScopeHandler { + constructor() { + super(new LineScopeHandler()); } - protected createTarget(parameters: CommonTargetParameters): Target { - return new TokenTarget(parameters); + get scopeType(): ScopeType { + return { type: "token" }; } - protected getContainingIndicesForPosition( - position: Position, - targets: Target[] - ): ContainingIndices | undefined { - const mappings = targets - .map((target, index) => ({ target, index })) - .filter((mapping) => mapping.target.contentRange.contains(position)); + protected getRegex(editor: TextEditor, _domain: Range) { + return getMatcher(editor.document.languageId).tokenMatcher; + } - if (mappings.length === 0) { - return undefined; - } + protected isPreferredOver( + editor: TextEditor, + scope1: TargetScope, + scope2: TargetScope + ): boolean | undefined { + return isPreferredOver(editor, scope1.domain, scope2.domain); + } - if (mappings.length > 1) { - const languageId = mappings[0].target.editor.document.languageId; - const { identifierMatcher } = getMatcher(languageId); + protected constructTarget( + isReversed: boolean, + editor: TextEditor, + contentRange: Range + ): Target { + return new TokenTarget({ + editor, + contentRange, + isReversed, + }); + } +} - // If multiple matches sort and take the first - mappings.sort(({ target: a }, { target: b }) => { - const textA = a.contentText; - const textB = b.contentText; +/** + * Determines whether token {@link a} is preferred over {@link b}. + * @param editor The editor containing {@link a} and {@link b} + * @param a A token range + * @param b A token range + * @returns `true` if token {@link a} is preferred over {@link b}; `false` if + * token {@link b} is preferred over {@link a}; `undefined` otherwise + */ +function isPreferredOver( + editor: TextEditor, + a: Range, + b: Range +): boolean | undefined { + const { document } = editor; + const { identifierMatcher } = getMatcher(document.languageId); - // First sort on identifier(alphanumeric) - const aIsAlphaNum = identifierMatcher.test(textA); - const bIsAlphaNum = identifierMatcher.test(textB); - if (aIsAlphaNum && !bIsAlphaNum) { - return -1; - } - if (bIsAlphaNum && !aIsAlphaNum) { - return 1; - } - // Second sort on length - const lengthDiff = textB.length - textA.length; - if (lengthDiff !== 0) { - return lengthDiff; - } - // Lastly sort on start position in reverse. ie prefer rightmost - return b.contentRange.start.compareTo(a.contentRange.start); - }); - } + // If multiple matches sort and take the first + const textA = document.getText(a); + const textB = document.getText(b); - const index = mappings[0].index; + // First sort on identifier(alphanumeric) + const aIsAlphaNum = identifierMatcher.test(textA); + const bIsAlphaNum = identifierMatcher.test(textB); - return { start: index, end: index }; + if (aIsAlphaNum && !bIsAlphaNum) { + return true; } + + if (bIsAlphaNum && !aIsAlphaNum) { + return false; + } + + // Second sort on length + const lengthDiff = textA.length - textB.length; + if (lengthDiff !== 0) { + return lengthDiff > 0 ? true : false; + } + + // Otherwise no preference + return undefined; } diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index afb7baa20a..54c9d91612 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -1,17 +1,30 @@ import { Position, Range, TextEditor } from "vscode"; +import { ScopeType } from "../../../core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types"; import { Target } from "../../../typings/target.types"; import { Direction } from "../../../typings/targetDescriptor.types"; /** - * Represents a scope, which is a specific instantiation of a scope type, - * eg a specific function, or a specific line or range of lines. Contains - * {@link target}, which represents the actual scope, as well as {@link domain}, - * which represents the range within which the given scope is canonical. For - * example, a scope representing the type of a parameter will have the entire - * parameter as its domain, so that one can say "take type" from anywhere - * within the parameter. + * A range in the document within which a particular scope type is considered + * to own the given region. We use this type both to define the domain within + * which a target is canonical, and the domain within which an iteration scope + * is canonical. */ export interface Scope { + /** + * The text editor containing {@link domain}. + */ + editor: TextEditor; + + /** + * This fuction can be defined to indicate how to choose between adjacent + * scopes. If the input target is zero width, and between two adjacent + * scopes, this funciton will be used to decide which scope is considered to + * contain the input target. If this function is `undefined`, or returns + * `undefined`, then the one to the right will be preferred. + * @param other The scope to compare to + */ + isPreferredOver?(other: Scope): boolean | undefined; + /** * The domain within which this scope is considered the canonical instance of * this scope type. For example, if the scope type represents a `key` in a @@ -20,9 +33,25 @@ export interface Scope { * * Most scopes will have a domain that is just the content range or removal * range of the scope. + * + * For an iteration scope, indicates the domain within which this iteration scope is considered the canonical + * iteration scope for the given scope type. For example, if the scope type + * is function, then the domain might be a class, so that "take every funk" + * works from anywhere within the given class. */ domain: Range; +} +/** + * Represents a scope, which is a specific instantiation of a scope type, + * eg a specific function, or a specific line or range of lines. Contains + * {@link target}, which represents the actual scope, as well as {@link domain}, + * which represents the range within which the given scope is canonical. For + * example, a scope representing the type of a parameter will have the entire + * parameter as its domain, so that one can say "take type" from anywhere + * within the parameter. + */ +export interface TargetScope extends Scope { /** * The target that represents this scope. Note that the target can represent * a contiguous range of instances of the given scope type, eg a range from @@ -41,21 +70,13 @@ export interface Scope { * be the functions in the class. This way one can say "take every funk" from * anywhere within the class. */ -export interface IterationScope { - /** - * The domain within which this iteration scope is considered the canonical - * iteration scope for the given scope type. For example, if the scope type - * is function, then the domain might be a class, so that "take every funk" - * works from anywhere within the given class. - */ - domain: Range; - +export interface IterationScope extends Scope { /** * The scopes in the given iteration scope. Note that each scope has its own * domain. We make this a function so that the scopes can be returned * lazily. */ - scopes: Scope[]; + scopes: TargetScope[]; } /** @@ -66,34 +87,64 @@ export interface IterationScope { */ export interface ScopeHandler { /** - * Given a position in a document, find the smallest scope containing the + * The scope type handled by this scope handler + */ + scopeType: ScopeType; + + /** + * Given a position in a document, find the smallest scope(s) containing the * given position. A scope is considered to contain the position even if it is - * adjacent to the position. If the position is adjacent to two scopes, prefer - * the one to the right. If no scope contains the given position, return - * `undefined`. + * adjacent to the position. If the position is adjacent to two scopes, return + * both. You can use {@link TargetScope.isPreferredOver} to indicate which one + * should have precedence. If no scope contains the given position, throw + * {@link NoContainingScopeError}. + * @param editor The editor containing {@link position} * @param position The position from which to expand + * @throws {NoContainingScopeError} If no scope contains the given position */ - getScopeContainingPosition(editor: TextEditor, position: Position): Scope; + getScopesContainingPosition( + editor: TextEditor, + position: Position + ): TargetScope[]; - getScopesIntersectingRange(editor: TextEditor, range: Range): Scope[]; + getScopesIntersectingRange(editor: TextEditor, range: Range): TargetScope[]; /** * Returns the iteration scope containing {@link position}. For example, if - * the position is inside a class, the iteration scope could contain a list - * of functions in the class. + * the position is inside a class, the iteration scope could contain a list of + * functions in the class. A scope is considered to contain the position even + * if it is adjacent to the position. If the position is adjacent to two + * scopes, return both. You can use {@link TargetScope.isPreferredOver} to + * indicate which one should have precedence. If no iteration scope contains the given + * position, throw {@link NoContainingScopeError}. * * @param editor The editor containing {@link position} * @param position The position from which to expand + * @throws {NoContainingScopeError} If no iteration scope contains the given + * position */ - getIterationScopeContainingPosition( + getIterationScopesContainingPosition( editor: TextEditor, position: Position - ): IterationScope; + ): IterationScope[]; + /** + * Returns a scope relative to {@link position}. If {@link direction} is + * `"forward"`, consider all scopes whose {@link Scope.domain.start} is equal + * or after {@link position}. If {@link direction} is `"backward"`, consider + * all scopes whose {@link Scope.domain.end} is equal or before + * {@link position}. Note that {@link offset} will always be greater than or + * equal to 1. An {@link offset} of 1 should return the first scope after + * {@link position} (before if {@link direction} is `"backward"`) + * @param editor The editor containing {@link position} + * @param position The position from which to start + * @param offset Which scope before / after position to return + * @param direction The direction to go relative to {@link position} + */ getScopeRelativeToPosition( editor: TextEditor, position: Position, offset: number, direction: Direction - ): Scope; + ): TargetScope; } diff --git a/src/processTargets/modifiers/scopeTypeStages/LineStage.ts b/src/processTargets/modifiers/scopeTypeStages/LineStage.ts deleted file mode 100644 index 2947c6ded0..0000000000 --- a/src/processTargets/modifiers/scopeTypeStages/LineStage.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Range, TextEditor } from "vscode"; -import type { Target } from "../../../typings/target.types"; -import type { - ContainingScopeModifier, - EveryScopeModifier, -} from "../../../typings/targetDescriptor.types"; -import type { ProcessedTargetsContext } from "../../../typings/Types"; -import type { ModifierStage } from "../../PipelineStages.types"; -import { LineTarget } from "../../targets"; - -export default class implements ModifierStage { - constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} - - run(context: ProcessedTargetsContext, target: Target): LineTarget[] { - if (this.modifier.type === "everyScope") { - return this.getEveryTarget(target); - } - return [toLineTarget(target)]; - } - - private getEveryTarget(target: Target): LineTarget[] { - const { contentRange, editor } = target; - const startLine = target.hasExplicitRange ? contentRange.start.line : 0; - const endLine = target.hasExplicitRange - ? contentRange.end.line - : editor.document.lineCount - 1; - const targets: LineTarget[] = []; - - for (let i = startLine; i <= endLine; ++i) { - targets.push( - createLineTarget( - target.editor, - target.isReversed, - editor.document.lineAt(i).range - ) - ); - } - - if (targets.length === 0) { - throw new Error( - `Couldn't find containing ${this.modifier.scopeType.type}` - ); - } - - return targets; - } -} - -function toLineTarget(target: Target): LineTarget { - return createLineTarget( - target.editor, - target.isReversed, - target.contentRange - ); -} - -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); - const endCharacterIndex = - endLine.range.end.character - - (endLine.text.length - endLine.text.trimEnd().length); - - return new Range( - startLine.lineNumber, - startLine.firstNonWhitespaceCharacterIndex, - endLine.lineNumber, - endCharacterIndex - ); -} diff --git a/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts b/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts index 36f144de4e..3787c40f24 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts @@ -7,7 +7,7 @@ import type { import type { ProcessedTargetsContext } from "../../../typings/Types"; import type { ModifierStage } from "../../PipelineStages.types"; import { ParagraphTarget } from "../../targets"; -import { fitRangeToLineContent } from "./LineStage"; +import { fitRangeToLineContent } from "../scopeHandlers/LineScopeHandler"; export default class implements ModifierStage { constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} From 3390b1ffb6e0f4505c90d36b5de8c7d1e9f39e69 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 17:15:18 +0100 Subject: [PATCH 17/69] More work on this stuff --- .../scopeHandlers/LineScopeHandler.ts | 2 +- ...eHandler.ts => SimpleRegexScopeHandler.ts} | 35 ++++----------- .../scopeHandlers/TokenScopeHandler.ts | 43 ++++++++----------- .../scopeHandlers/scopeHandler.types.ts | 20 ++++----- .../recorded/scopes/line/clearTwoLines.yml | 28 ++++++++++++ .../recorded/subtoken/takeEveryWordLine.yml | 35 +++++++++++++++ 6 files changed, 102 insertions(+), 61 deletions(-) rename src/processTargets/modifiers/scopeHandlers/{BaseRegexScopeHandler.ts => SimpleRegexScopeHandler.ts} (55%) create mode 100644 src/test/suite/fixtures/recorded/scopes/line/clearTwoLines.yml create mode 100644 src/test/suite/fixtures/recorded/subtoken/takeEveryWordLine.yml diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index ae3f056999..9b6b44dc55 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -57,7 +57,7 @@ export default class LineScopeHandler implements ScopeHandler { direction === "forward" ? position.line + offset : position.line - offset; if (lineNumber < 0 || lineNumber >= editor.document.lineCount) { - throw new NoContainingScopeError("line"); + throw new NoContainingScopeError(this.scopeType.type); } return lineNumberToScope(editor, lineNumber); diff --git a/src/processTargets/modifiers/scopeHandlers/BaseRegexScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SimpleRegexScopeHandler.ts similarity index 55% rename from src/processTargets/modifiers/scopeHandlers/BaseRegexScopeHandler.ts rename to src/processTargets/modifiers/scopeHandlers/SimpleRegexScopeHandler.ts index 5ff09596d8..adeac2604e 100644 --- a/src/processTargets/modifiers/scopeHandlers/BaseRegexScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/SimpleRegexScopeHandler.ts @@ -3,51 +3,34 @@ import { Target } from "../../../typings/target.types"; import NestedScopeHandler from "./NestedScopeHandler"; import { ScopeHandler, TargetScope } from "./scopeHandler.types"; -export default abstract class BaseRegexScopeHandler extends NestedScopeHandler { - constructor(parentScopeHandler: ScopeHandler) { +export default abstract class SimpleRegexScopeHandler extends NestedScopeHandler { + constructor(parentScopeHandler: ScopeHandler, private regex: RegExp) { super(parentScopeHandler); } - protected abstract getRegex(editor: TextEditor, domain: Range): RegExp; - protected abstract constructTarget( isReversed: boolean, editor: TextEditor, contentRange: Range ): Target; - protected isPreferredOver( - _editor: TextEditor, - _scope1: TargetScope, - _scope2: TargetScope - ): boolean | undefined { - return undefined; - } - protected getScopesInParentScope({ editor, domain, }: TargetScope): TargetScope[] { - return this.getMatchesInRange(editor, domain).map((range) => { - const scope: TargetScope = { - editor, - domain: range, - getTarget: (isReversed) => - this.constructTarget(isReversed, editor, range), - }; - - scope.isPreferredOver = (other) => - this.isPreferredOver(editor, scope, other as TargetScope); - - return scope; - }); + return this.getMatchesInRange(editor, domain).map((range) => ({ + editor, + domain: range, + getTarget: (isReversed) => + this.constructTarget(isReversed, editor, range), + })); } private getMatchesInRange(editor: TextEditor, range: Range): Range[] { const offset = editor.document.offsetAt(range.start); const text = editor.document.getText(range); - return [...text.matchAll(this.getRegex(editor, range))].map( + return [...text.matchAll(this.regex)].map( (match) => new Range( editor.document.positionAt(offset + match.index!), diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index be5d30558c..918fd7fe0b 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -1,13 +1,13 @@ import { Range, TextEditor } from "vscode"; import { getMatcher } from "../../../core/tokenizer"; -import { Target } from "../../../typings/target.types"; import { ScopeType } from "../../../typings/targetDescriptor.types"; +import { getTokensInRange } from "../../../util/getTokensInRange"; import { TokenTarget } from "../../targets"; -import BaseRegexScopeHandler from "./BaseRegexScopeHandler"; import LineScopeHandler from "./LineScopeHandler"; +import NestedScopeHandler from "./NestedScopeHandler"; import { TargetScope } from "./scopeHandler.types"; -export default class TokenScopeHandler extends BaseRegexScopeHandler { +export default class TokenScopeHandler extends NestedScopeHandler { constructor() { super(new LineScopeHandler()); } @@ -16,28 +16,23 @@ export default class TokenScopeHandler extends BaseRegexScopeHandler { return { type: "token" }; } - protected getRegex(editor: TextEditor, _domain: Range) { - return getMatcher(editor.document.languageId).tokenMatcher; - } - - protected isPreferredOver( - editor: TextEditor, - scope1: TargetScope, - scope2: TargetScope - ): boolean | undefined { - return isPreferredOver(editor, scope1.domain, scope2.domain); - } - - protected constructTarget( - isReversed: boolean, - editor: TextEditor, - contentRange: Range - ): Target { - return new TokenTarget({ + protected getScopesInParentScope({ + editor, + domain, + }: TargetScope): TargetScope[] { + return getTokensInRange(editor, domain).map(({ range }) => ({ editor, - contentRange, - isReversed, - }); + domain: range, + getTarget: (isReversed) => + new TokenTarget({ + editor, + contentRange: range, + isReversed, + }), + isPreferredOver(other) { + return isPreferredOver(editor, range, other.domain); + }, + })); } } diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 54c9d91612..4b14df3435 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -15,16 +15,6 @@ export interface Scope { */ editor: TextEditor; - /** - * This fuction can be defined to indicate how to choose between adjacent - * scopes. If the input target is zero width, and between two adjacent - * scopes, this funciton will be used to decide which scope is considered to - * contain the input target. If this function is `undefined`, or returns - * `undefined`, then the one to the right will be preferred. - * @param other The scope to compare to - */ - isPreferredOver?(other: Scope): boolean | undefined; - /** * The domain within which this scope is considered the canonical instance of * this scope type. For example, if the scope type represents a `key` in a @@ -40,6 +30,16 @@ export interface Scope { * works from anywhere within the given class. */ domain: Range; + + /** + * This fuction can be defined to indicate how to choose between adjacent + * scopes. If the input target is zero width, and between two adjacent + * scopes, this funciton will be used to decide which scope is considered to + * contain the input target. If this function is `undefined`, or returns + * `undefined`, then the one to the right will be preferred. + * @param other The scope to compare to + */ + isPreferredOver?(other: Scope): boolean | undefined; } /** diff --git a/src/test/suite/fixtures/recorded/scopes/line/clearTwoLines.yml b/src/test/suite/fixtures/recorded/scopes/line/clearTwoLines.yml new file mode 100644 index 0000000000..e91875f26a --- /dev/null +++ b/src/test/suite/fixtures/recorded/scopes/line/clearTwoLines.yml @@ -0,0 +1,28 @@ +languageId: plaintext +command: + spokenForm: clear two lines + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: line} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + aaa bbb + ccc ddd + selections: + - anchor: {line: 0, character: 7} + active: {line: 0, character: 7} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: line}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/subtoken/takeEveryWordLine.yml b/src/test/suite/fixtures/recorded/subtoken/takeEveryWordLine.yml new file mode 100644 index 0000000000..7a8f59ec4d --- /dev/null +++ b/src/test/suite/fixtures/recorded/subtoken/takeEveryWordLine.yml @@ -0,0 +1,35 @@ +languageId: plaintext +command: + spokenForm: take every word line + version: 3 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: word} + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: |- + aaaBbb cccDdd + eee + selections: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 13} + marks: {} +finalState: + documentContents: |- + aaaBbb cccDdd + eee + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 3} + - anchor: {line: 0, character: 3} + active: {line: 0, character: 6} + - anchor: {line: 0, character: 7} + active: {line: 0, character: 10} + - anchor: {line: 0, character: 10} + active: {line: 0, character: 13} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: word}}, {type: containingScope, scopeType: {type: line}}]}] From 21d75ea99c5869b61ca780b4baeba6bcc0ee86b5 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 17:58:43 +0100 Subject: [PATCH 18/69] Rename and add some todos --- .../modifiers/ContainingScopeStage.ts | 4 +- .../modifiers/EveryScopeStage.ts | 2 +- .../modifiers/RelativeScopeStage.ts | 2 +- .../scopeHandlers/LineScopeHandler.ts | 4 +- .../scopeHandlers/NestedScopeHandler.ts | 10 ++-- .../SurroundingPairScopeHandler.ts | 47 +++++++++++++++++++ .../scopeHandlers/scopeHandler.types.ts | 4 +- 7 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index febd1e1645..682076b79a 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -51,7 +51,7 @@ export class ContainingScopeStage implements ModifierStage { const { scopeType } = this.modifier; const scopeHandler = getScopeHandler(scopeType); - const startScopes = scopeHandler.getScopesContainingPosition(editor, start); + const startScopes = scopeHandler.getScopesIntersectingPosition(editor, start); if (end.isEqual(start)) { return [getPreferredScope(startScopes).getTarget(isReversed)]; @@ -63,7 +63,7 @@ export class ContainingScopeStage implements ModifierStage { return [startScope.getTarget(isReversed)]; } - const endScopes = scopeHandler.getScopesContainingPosition(editor, end); + const endScopes = scopeHandler.getScopesIntersectingPosition(editor, end); const endScope = getLeftScope(endScopes); return constructScopeRangeTarget(isReversed, startScope, endScope); diff --git a/src/processTargets/modifiers/EveryScopeStage.ts b/src/processTargets/modifiers/EveryScopeStage.ts index 9506a351c4..9cd978d07a 100644 --- a/src/processTargets/modifiers/EveryScopeStage.ts +++ b/src/processTargets/modifiers/EveryScopeStage.ts @@ -52,7 +52,7 @@ export class EveryScopeStage implements ModifierStage { const { start, end } = range; - const startScopes = scopeHandler.getIterationScopesContainingPosition( + const startScopes = scopeHandler.getIterationScopesIntersectingPosition( editor, start ); diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index f9c16458bd..b0b1dcdfa7 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -135,7 +135,7 @@ function getIndex0Scopes( return range.isEmpty ? [ getPreferredScope( - scopeHandler.getScopesContainingPosition(editor, range.start) + scopeHandler.getScopesIntersectingPosition(editor, range.start) ), ] : scopeHandler.getScopesIntersectingRange(editor, range); diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index 9b6b44dc55..85020d64d0 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -16,7 +16,7 @@ export default class LineScopeHandler implements ScopeHandler { return { type: "line" }; } - getScopesContainingPosition( + getScopesIntersectingPosition( editor: TextEditor, position: Position ): TargetScope[] { @@ -32,7 +32,7 @@ export default class LineScopeHandler implements ScopeHandler { ); } - getIterationScopesContainingPosition( + getIterationScopesIntersectingPosition( editor: TextEditor, _position: Position ): IterationScope[] { diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index 3e61ac2943..586298f286 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -17,12 +17,12 @@ export default abstract class NestedScopeHandler implements ScopeHandler { parentScope: TargetScope ): TargetScope[]; - getScopesContainingPosition( + getScopesIntersectingPosition( editor: TextEditor, position: Position ): TargetScope[] { const parentScope = getPreferredScope( - this.parentScopeHandler.getScopesContainingPosition(editor, position) + this.parentScopeHandler.getScopesIntersectingPosition(editor, position) ); return this.getScopesInParentScope(parentScope).filter(({ domain }) => @@ -40,12 +40,12 @@ export default abstract class NestedScopeHandler implements ScopeHandler { }); } - getIterationScopesContainingPosition( + getIterationScopesIntersectingPosition( editor: TextEditor, position: Position ): IterationScope[] { return this.parentScopeHandler - .getScopesContainingPosition(editor, position) + .getScopesIntersectingPosition(editor, position) .map((parentScope) => ({ domain: parentScope.domain, editor, @@ -82,7 +82,7 @@ export default abstract class NestedScopeHandler implements ScopeHandler { direction: Direction ): Generator { const containingParentScope = getPreferredScope( - this.parentScopeHandler.getScopesContainingPosition(editor, position) + this.parentScopeHandler.getScopesIntersectingPosition(editor, position) ); yield this.getScopesInParentScope(containingParentScope).filter( diff --git a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts new file mode 100644 index 0000000000..f1689b7d56 --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts @@ -0,0 +1,47 @@ +import { TextEditor, Position, Range } from "vscode"; +import { ScopeType } from "../../../core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types"; +import { Direction } from "../../../typings/targetDescriptor.types"; +import { + IterationScope, + ScopeHandler, + TargetScope, +} from "./scopeHandler.types"; + +export default class SurroundingPairScopeHandler implements ScopeHandler { + scopeType: ScopeType; + + getScopesIntersectingPosition( + editor: TextEditor, + position: Position + ): TargetScope[] { + // TODO: Run existing surrounding pair code on empty range constructed from + // position, returning both if position is adjacent to to + throw new Error("Method not implemented."); + } + + getScopesIntersectingRange(editor: TextEditor, range: Range): TargetScope[] { + // TODO: Implement https://github.com/cursorless-dev/cursorless/pull/1031#issuecomment-1276777449 + throw new Error("Method not implemented."); + } + + getIterationScopesIntersectingPosition( + editor: TextEditor, + position: Position + ): IterationScope[] { + // TODO: Return inside strict containing pair + throw new Error("Method not implemented."); + } + + getScopeRelativeToPosition( + editor: TextEditor, + position: Position, + offset: number, + direction: Direction + ): TargetScope { + // TODO: Walk forward until we hit either an opening or closing delimiter. + // If we hit an opening delimiter then we walk over as many pairs as we need + // to until we have offset. If we *first* instead hit a closing PR en then we + // expand containing and walk forward from that + throw new Error("Method not implemented."); + } +} diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 4b14df3435..7098ea0766 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -102,7 +102,7 @@ export interface ScopeHandler { * @param position The position from which to expand * @throws {NoContainingScopeError} If no scope contains the given position */ - getScopesContainingPosition( + getScopesIntersectingPosition( editor: TextEditor, position: Position ): TargetScope[]; @@ -123,7 +123,7 @@ export interface ScopeHandler { * @throws {NoContainingScopeError} If no iteration scope contains the given * position */ - getIterationScopesContainingPosition( + getIterationScopesIntersectingPosition( editor: TextEditor, position: Position ): IterationScope[]; From bde75313e0dd161a53ef0834189bb1c88f56de6e Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:31:41 +0100 Subject: [PATCH 19/69] More stuff --- .../modifiers/ContainingScopeStage.ts | 5 ++- .../modifiers/RelativeScopeStage.ts | 16 ++++---- .../modifiers/getPreferredScope.ts | 22 ++++++----- .../scopeHandlers/NestedScopeHandler.ts | 37 +++++++++++-------- .../scopeHandlers/scopeHandler.types.ts | 5 +-- .../relativeScopes/clearNextToken.yml | 32 ++++++++++++++++ .../relativeScopes/clearNextTwoTokens.yml | 30 +++++++++++++++ .../relativeScopes/clearPreviousToken.yml | 32 ++++++++++++++++ .../relativeScopes/clearPreviousTwoTokens.yml | 31 ++++++++++++++++ .../relativeScopes/clearSecondNextToken.yml | 31 ++++++++++++++++ .../clearSecondPreviousToken.yml | 32 ++++++++++++++++ .../relativeScopes/clearTwoTokens.yml | 31 ++++++++++++++++ .../relativeScopes/clearTwoTokensBackward.yml | 30 +++++++++++++++ 13 files changed, 298 insertions(+), 36 deletions(-) create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearNextToken.yml create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearNextTwoTokens.yml create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearPreviousToken.yml create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoTokens.yml create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearSecondNextToken.yml create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearSecondPreviousToken.yml create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokens.yml create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokensBackward.yml diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index 682076b79a..77933248cd 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -51,7 +51,10 @@ export class ContainingScopeStage implements ModifierStage { const { scopeType } = this.modifier; const scopeHandler = getScopeHandler(scopeType); - const startScopes = scopeHandler.getScopesIntersectingPosition(editor, start); + const startScopes = scopeHandler.getScopesIntersectingPosition( + editor, + start + ); if (end.isEqual(start)) { return [getPreferredScope(startScopes).getTarget(isReversed)]; diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index b0b1dcdfa7..5db161b0fb 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -132,11 +132,13 @@ function getIndex0Scopes( editor: TextEditor, range: Range ): TargetScope[] { - return range.isEmpty - ? [ - getPreferredScope( - scopeHandler.getScopesIntersectingPosition(editor, range.start) - ), - ] - : scopeHandler.getScopesIntersectingRange(editor, range); + if (range.isEmpty) { + const preferredScope = getPreferredScope( + scopeHandler.getScopesIntersectingPosition(editor, range.start) + ); + + return preferredScope == null ? [] : [preferredScope]; + } + + return scopeHandler.getScopesIntersectingRange(editor, range); } diff --git a/src/processTargets/modifiers/getPreferredScope.ts b/src/processTargets/modifiers/getPreferredScope.ts index dc1611774e..bb349c86cb 100644 --- a/src/processTargets/modifiers/getPreferredScope.ts +++ b/src/processTargets/modifiers/getPreferredScope.ts @@ -1,7 +1,7 @@ import { Scope } from "./scopeHandlers/scopeHandler.types"; -export function getPreferredScope(scopes: T[]): T { - return getPreferredScopeHelper( +export function getPreferredScope(scopes: T[]): T | undefined { + return getScopeHelper( scopes, (scope1, scope2) => scope1.isPreferredOver?.(scope2) ?? @@ -9,27 +9,31 @@ export function getPreferredScope(scopes: T[]): T { ); } -export function getLeftScope(scopes: T[]): T { - return getPreferredScopeHelper(scopes, (scope1, scope2) => +export function getLeftScope(scopes: T[]): T | undefined { + return getScopeHelper(scopes, (scope1, scope2) => scope1.domain.start.isBefore(scope2.domain.start) ); } -export function getRightScope(scopes: T[]): T { - return getPreferredScopeHelper(scopes, (scope1, scope2) => +export function getRightScope(scopes: T[]): T | undefined { + return getScopeHelper(scopes, (scope1, scope2) => scope1.domain.start.isAfter(scope2.domain.start) ); } -function getPreferredScopeHelper( +function getScopeHelper( scopes: T[], isScope1Preferred: (scope1: Scope, scope2: Scope) => boolean -): T { +): T | undefined { + if (scopes.length === 0) { + return undefined; + } + if (scopes.length === 1) { return scopes[0]; } - if (scopes.length !== 2) { + if (scopes.length > 2) { throw Error("Cannot compare more than two scopes."); } diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index 586298f286..e064366fd1 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -25,6 +25,10 @@ export default abstract class NestedScopeHandler implements ScopeHandler { this.parentScopeHandler.getScopesIntersectingPosition(editor, position) ); + if (parentScope == null) { + return []; + } + return this.getScopesInParentScope(parentScope).filter(({ domain }) => domain.contains(position) ); @@ -85,24 +89,27 @@ export default abstract class NestedScopeHandler implements ScopeHandler { this.parentScopeHandler.getScopesIntersectingPosition(editor, position) ); - yield this.getScopesInParentScope(containingParentScope).filter( - ({ domain }) => - direction === "forward" - ? domain.start.isAfterOrEqual(position) - : domain.end.isBeforeOrEqual(position) - ); + let currentPosition = position; + + if (containingParentScope != null) { + yield this.getScopesInParentScope(containingParentScope).filter( + ({ domain }) => + direction === "forward" + ? domain.start.isAfterOrEqual(position) + : domain.end.isBeforeOrEqual(position) + ); - let parentOffset = 1; - let currentPosition = - direction === "forward" - ? containingParentScope.domain.start - : containingParentScope.domain.end; + currentPosition = + direction === "forward" + ? containingParentScope.domain.end + : containingParentScope.domain.start; + } while (true) { const parentScope = this.parentScopeHandler.getScopeRelativeToPosition( editor, currentPosition, - parentOffset, + 1, direction ); @@ -110,10 +117,8 @@ export default abstract class NestedScopeHandler implements ScopeHandler { currentPosition = direction === "forward" - ? parentScope.domain.start - : parentScope.domain.end; - - parentOffset += 1; + ? parentScope.domain.end + : parentScope.domain.start; } } } diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 7098ea0766..416f76a4b8 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -96,11 +96,10 @@ export interface ScopeHandler { * given position. A scope is considered to contain the position even if it is * adjacent to the position. If the position is adjacent to two scopes, return * both. You can use {@link TargetScope.isPreferredOver} to indicate which one - * should have precedence. If no scope contains the given position, throw - * {@link NoContainingScopeError}. + * should have precedence. If no scope contains the given position, return + * an empty list. * @param editor The editor containing {@link position} * @param position The position from which to expand - * @throws {NoContainingScopeError} If no scope contains the given position */ getScopesIntersectingPosition( editor: TextEditor, diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearNextToken.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearNextToken.yml new file mode 100644 index 0000000000..6c03889259 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearNextToken.yml @@ -0,0 +1,32 @@ +languageId: markdown +command: + spokenForm: clear next token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + aaa + bbb + ccc + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: |- + aaa + + ccc + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 1, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearNextTwoTokens.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearNextTwoTokens.yml new file mode 100644 index 0000000000..b72eefe97c --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearNextTwoTokens.yml @@ -0,0 +1,30 @@ +languageId: markdown +command: + spokenForm: clear next two tokens + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 2 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + aaa + bbb + ccc + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: | + aaa + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousToken.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousToken.yml new file mode 100644 index 0000000000..339e4dc55b --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousToken.yml @@ -0,0 +1,32 @@ +languageId: markdown +command: + spokenForm: clear previous token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + aaa + bbb + ccc + selections: + - anchor: {line: 2, character: 1} + active: {line: 2, character: 1} + marks: {} +finalState: + documentContents: |- + aaa + + ccc + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 1, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoTokens.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoTokens.yml new file mode 100644 index 0000000000..5a36637c64 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoTokens.yml @@ -0,0 +1,31 @@ +languageId: markdown +command: + spokenForm: clear previous two tokens + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 2 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + aaa + bbb + ccc + selections: + - anchor: {line: 2, character: 1} + active: {line: 2, character: 1} + marks: {} +finalState: + documentContents: |- + + ccc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearSecondNextToken.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearSecondNextToken.yml new file mode 100644 index 0000000000..103a6bd0d6 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearSecondNextToken.yml @@ -0,0 +1,31 @@ +languageId: markdown +command: + spokenForm: clear second next token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 2 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + aaa + bbb + ccc + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: | + aaa + bbb + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 2, length: 1, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearSecondPreviousToken.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearSecondPreviousToken.yml new file mode 100644 index 0000000000..26221db80a --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearSecondPreviousToken.yml @@ -0,0 +1,32 @@ +languageId: markdown +command: + spokenForm: clear second previous token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 2 + length: 1 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + aaa + bbb + ccc + selections: + - anchor: {line: 2, character: 1} + active: {line: 2, character: 1} + marks: {} +finalState: + documentContents: |- + + bbb + ccc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 2, length: 1, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokens.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokens.yml new file mode 100644 index 0000000000..ba4bff468d --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokens.yml @@ -0,0 +1,31 @@ +languageId: markdown +command: + spokenForm: clear two tokens + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + aaa + bbb + ccc + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: |- + + ccc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokensBackward.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokensBackward.yml new file mode 100644 index 0000000000..537b2e2672 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokensBackward.yml @@ -0,0 +1,30 @@ +languageId: markdown +command: + spokenForm: clear two tokens backward + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + aaa + bbb + ccc + selections: + - anchor: {line: 2, character: 1} + active: {line: 2, character: 1} + marks: {} +finalState: + documentContents: | + aaa + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] From a8104f66622cbc1c2af61491449d6e93ce41d311 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:57:01 +0100 Subject: [PATCH 20/69] Tweaks --- src/processTargets/modifiers/RelativeScopeStage.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index 5db161b0fb..fdc645174a 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -37,9 +37,16 @@ export class RelativeScopeStage implements ModifierStage { const index0Scopes = getIndex0Scopes(scopeHandler, editor, range); + const initialPosition = + index0Scopes.length > 0 + ? getIndex0DistalPosition(direction, index0Scopes) + : direction === "forward" + ? range.end + : range.start; + const proximalScope = scopeHandler.getScopeRelativeToPosition( editor, - getIndex0DistalPosition(direction, index0Scopes), + initialPosition, offset, direction ); From e24a9e3ad0fd632ee4b03f5a4c21e0b05c8d2943 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 20:52:05 +0100 Subject: [PATCH 21/69] Renames and docstrings --- .../modifiers/ContainingScopeStage.ts | 4 +- .../modifiers/EveryScopeStage.ts | 4 +- .../modifiers/RelativeScopeStage.ts | 4 +- .../scopeHandlers/LineScopeHandler.ts | 6 +- .../scopeHandlers/NestedScopeHandler.ts | 14 +-- .../SurroundingPairScopeHandler.ts | 6 +- .../scopeHandlers/scopeHandler.types.ts | 110 +++++++++++++----- 7 files changed, 101 insertions(+), 47 deletions(-) diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index 77933248cd..258c9c9739 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -51,7 +51,7 @@ export class ContainingScopeStage implements ModifierStage { const { scopeType } = this.modifier; const scopeHandler = getScopeHandler(scopeType); - const startScopes = scopeHandler.getScopesIntersectingPosition( + const startScopes = scopeHandler.getScopesTouchingPosition( editor, start ); @@ -66,7 +66,7 @@ export class ContainingScopeStage implements ModifierStage { return [startScope.getTarget(isReversed)]; } - const endScopes = scopeHandler.getScopesIntersectingPosition(editor, end); + const endScopes = scopeHandler.getScopesTouchingPosition(editor, end); const endScope = getLeftScope(endScopes); return constructScopeRangeTarget(isReversed, startScope, endScope); diff --git a/src/processTargets/modifiers/EveryScopeStage.ts b/src/processTargets/modifiers/EveryScopeStage.ts index 9cd978d07a..df6064f98d 100644 --- a/src/processTargets/modifiers/EveryScopeStage.ts +++ b/src/processTargets/modifiers/EveryScopeStage.ts @@ -41,7 +41,7 @@ export class EveryScopeStage implements ModifierStage { const scopeHandler = getScopeHandler(scopeType); if (target.hasExplicitRange) { - const scopes = scopeHandler.getScopesIntersectingRange(editor, range); + const scopes = scopeHandler.getScopesOverlappingRange(editor, range); if (scopes.length === 0) { throw new NoContainingScopeError(scopeType.type); @@ -52,7 +52,7 @@ export class EveryScopeStage implements ModifierStage { const { start, end } = range; - const startScopes = scopeHandler.getIterationScopesIntersectingPosition( + const startScopes = scopeHandler.getIterationScopesTouchingPosition( editor, start ); diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index fdc645174a..c65b62dddb 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -141,11 +141,11 @@ function getIndex0Scopes( ): TargetScope[] { if (range.isEmpty) { const preferredScope = getPreferredScope( - scopeHandler.getScopesIntersectingPosition(editor, range.start) + scopeHandler.getScopesTouchingPosition(editor, range.start) ); return preferredScope == null ? [] : [preferredScope]; } - return scopeHandler.getScopesIntersectingRange(editor, range); + return scopeHandler.getScopesOverlappingRange(editor, range); } diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index 85020d64d0..01b51c41e8 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -16,14 +16,14 @@ export default class LineScopeHandler implements ScopeHandler { return { type: "line" }; } - getScopesIntersectingPosition( + getScopesTouchingPosition( editor: TextEditor, position: Position ): TargetScope[] { return [lineNumberToScope(editor, position.line)]; } - getScopesIntersectingRange( + getScopesOverlappingRange( editor: TextEditor, { start, end }: Range ): TargetScope[] { @@ -32,7 +32,7 @@ export default class LineScopeHandler implements ScopeHandler { ); } - getIterationScopesIntersectingPosition( + getIterationScopesTouchingPosition( editor: TextEditor, _position: Position ): IterationScope[] { diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index e064366fd1..497dfcc15b 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -17,12 +17,12 @@ export default abstract class NestedScopeHandler implements ScopeHandler { parentScope: TargetScope ): TargetScope[]; - getScopesIntersectingPosition( + getScopesTouchingPosition( editor: TextEditor, position: Position ): TargetScope[] { const parentScope = getPreferredScope( - this.parentScopeHandler.getScopesIntersectingPosition(editor, position) + this.parentScopeHandler.getScopesTouchingPosition(editor, position) ); if (parentScope == null) { @@ -34,9 +34,9 @@ export default abstract class NestedScopeHandler implements ScopeHandler { ); } - getScopesIntersectingRange(editor: TextEditor, range: Range): TargetScope[] { + getScopesOverlappingRange(editor: TextEditor, range: Range): TargetScope[] { return this.parentScopeHandler - .getScopesIntersectingRange(editor, range) + .getScopesOverlappingRange(editor, range) .flatMap((parentScope) => this.getScopesInParentScope(parentScope)) .filter(({ domain }) => { const intersection = domain.intersection(range); @@ -44,12 +44,12 @@ export default abstract class NestedScopeHandler implements ScopeHandler { }); } - getIterationScopesIntersectingPosition( + getIterationScopesTouchingPosition( editor: TextEditor, position: Position ): IterationScope[] { return this.parentScopeHandler - .getScopesIntersectingPosition(editor, position) + .getScopesTouchingPosition(editor, position) .map((parentScope) => ({ domain: parentScope.domain, editor, @@ -86,7 +86,7 @@ export default abstract class NestedScopeHandler implements ScopeHandler { direction: Direction ): Generator { const containingParentScope = getPreferredScope( - this.parentScopeHandler.getScopesIntersectingPosition(editor, position) + this.parentScopeHandler.getScopesTouchingPosition(editor, position) ); let currentPosition = position; diff --git a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts index f1689b7d56..4689d316b9 100644 --- a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts @@ -10,7 +10,7 @@ import { export default class SurroundingPairScopeHandler implements ScopeHandler { scopeType: ScopeType; - getScopesIntersectingPosition( + getScopesTouchingPosition( editor: TextEditor, position: Position ): TargetScope[] { @@ -19,12 +19,12 @@ export default class SurroundingPairScopeHandler implements ScopeHandler { throw new Error("Method not implemented."); } - getScopesIntersectingRange(editor: TextEditor, range: Range): TargetScope[] { + getScopesOverlappingRange(editor: TextEditor, range: Range): TargetScope[] { // TODO: Implement https://github.com/cursorless-dev/cursorless/pull/1031#issuecomment-1276777449 throw new Error("Method not implemented."); } - getIterationScopesIntersectingPosition( + getIterationScopesTouchingPosition( editor: TextEditor, position: Position ): IterationScope[] { diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 416f76a4b8..c44d836bb1 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -76,14 +76,31 @@ export interface IterationScope extends Scope { * domain. We make this a function so that the scopes can be returned * lazily. */ - scopes: TargetScope[]; + getScopes(): TargetScope[]; } /** - * Represents a scope type, containing functions that can be used to find + * Represents a scope type. The functions in this interface allow us to find * specific instances of the given scope type in a document. For example, it - * has a function to find the scope containing a given position, a function to - * find every instance of the scope in a range, etc. + * has a function to find the scopes touching a given position, a function to + * find every instance of the scope overlapping a range, etc. These functions + * are used by the various modifier stages to implement modifiers that involve + * the given scope type. + * + * Note that a scope type can be hierarchical, ie one scope of the given type + * can contain another scope of the same type. For example, a function can + * contain other functions, so functions are hierarchical. Surrounding pairs + * are also hierarchical, as they can be nested. Many scope types are not + * hierarchical, though, eg line, token, word, etc. + * + * In the case of a hierarchical scope type, these functions should never + * return scopes that contain one another. Ie if we return a surrounding pair, + * we shouldn't also return any surrounding pairs contained within, or if we + * return a function, we shouldn't also return a function nested within that + * function. + * + * Note that there are helpers that can sometimes be used to avoid implementing + * a scope handler from scratch, eg {@link NestedScopeHandler}. */ export interface ScopeHandler { /** @@ -92,49 +109,86 @@ export interface ScopeHandler { scopeType: ScopeType; /** - * Given a position in a document, find the smallest scope(s) containing the - * given position. A scope is considered to contain the position even if it is - * adjacent to the position. If the position is adjacent to two scopes, return - * both. You can use {@link TargetScope.isPreferredOver} to indicate which one - * should have precedence. If no scope contains the given position, return - * an empty list. + * Return all scope(s) touching the given position. A scope is considered to + * touch a position if its domain contains the position or is directly + * adjacent to the position. In other words, return all scopes for which the + * following is true: + * + * ```typescript + * scope.domain.start <= position && scope.domain.end >= position + * ``` + * + * If the position is directly adjacent to two scopes, return both. You can + * use {@link TargetScope.isPreferredOver} to indicate which one should have + * precedence. If no scope contains the given position, return an empty + * list. + * + * Note that if this scope type is hierarchical, return only minimal scopes, + * ie if scope A and scope B both touch {@link position}, and scope A contains + * scope B, return scope B but not scope A. * @param editor The editor containing {@link position} * @param position The position from which to expand */ - getScopesIntersectingPosition( + getScopesTouchingPosition( editor: TextEditor, position: Position ): TargetScope[]; - getScopesIntersectingRange(editor: TextEditor, range: Range): TargetScope[]; + /** + * Return a list of all scopes that overlap with {@link range}. A scope is + * considered to overlap with a range if its domain has a non-empty + * intersection with the range. In other words, return all scopes for which + * the following is true: + * + * ```typescript + * const intersection = scope.domain.intersection(range); + * return intersection != null && !intersection.isEmpty; + * ``` + * + * @param editor The editor containing {@link range} + * @param range The range with which to find overlapping scopes + */ + getScopesOverlappingRange(editor: TextEditor, range: Range): TargetScope[]; /** - * Returns the iteration scope containing {@link position}. For example, if - * the position is inside a class, the iteration scope could contain a list of - * functions in the class. A scope is considered to contain the position even - * if it is adjacent to the position. If the position is adjacent to two - * scopes, return both. You can use {@link TargetScope.isPreferredOver} to - * indicate which one should have precedence. If no iteration scope contains the given - * position, throw {@link NoContainingScopeError}. + * Returns all iteration scopes touching {@link position}. For example, if + * scope type is `namedFunction`, and {@link position} is inside a class, the + * iteration scope would contain a list of functions in the class. A scope + * is considered to touch a position if its domain contains the position or + * is directly adjacent to the position. In other words, return all iteration + * scopes for which the following is true: + * + * ```typescript + * iterationScope.domain.start <= position && iterationScope.domain.end >= position + * ``` + * + * If the position is directly adjacent to two iteration scopes, return both. + * You can use {@link TargetScope.isPreferredOver} to indicate which one + * should have precedence. If no iteration scope contains the given + * position, return an empty list. + * + * Note that if the iteration scope type is hierarchical, return only minimal + * scopes, ie if iteration scope A and iteration scope B both touch + * {@link position}, and iteration scope A contains iteration scope B, return + * iteration scope B but not iteration scope A. * * @param editor The editor containing {@link position} * @param position The position from which to expand - * @throws {NoContainingScopeError} If no iteration scope contains the given - * position */ - getIterationScopesIntersectingPosition( + getIterationScopesTouchingPosition( editor: TextEditor, position: Position ): IterationScope[]; /** - * Returns a scope relative to {@link position}. If {@link direction} is - * `"forward"`, consider all scopes whose {@link Scope.domain.start} is equal - * or after {@link position}. If {@link direction} is `"backward"`, consider - * all scopes whose {@link Scope.domain.end} is equal or before + * Returns a scope before or after {@link position}, depending on + * {@link direction}. If {@link direction} is `"forward"`, consider all + * scopes whose {@link Scope.domain.start} is equal or after + * {@link position}. If {@link direction} is `"backward"`, consider all + * scopes whose {@link Scope.domain.end} is equal or before * {@link position}. Note that {@link offset} will always be greater than or - * equal to 1. An {@link offset} of 1 should return the first scope after - * {@link position} (before if {@link direction} is `"backward"`) + * equal to 1. For example, an {@link offset} of 1 should return the first + * scope after {@link position} (before if {@link direction} is `"backward"`) * @param editor The editor containing {@link position} * @param position The position from which to start * @param offset Which scope before / after position to return From 99a5af6fc7eaaa3cc8484e19584cd911887f5dcf Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 20:54:49 +0100 Subject: [PATCH 22/69] Restructuring --- .../modifiers/RelativeScopeStage.ts | 3 +- .../modifiers/constructScopeRangeTarget.ts | 2 +- .../modifiers/getPreferredScope.ts | 2 +- .../scopeHandlers/LineScopeHandler.ts | 3 +- .../scopeHandlers/NestedScopeHandler.ts | 3 +- .../scopeHandlers/SimpleRegexScopeHandler.ts | 3 +- .../SurroundingPairScopeHandler.ts | 3 +- .../scopeHandlers/TokenScopeHandler.ts | 2 +- .../modifiers/scopeHandlers/scope.types.ts | 78 +++++++++++++++++++ .../scopeHandlers/scopeHandler.types.ts | 78 +------------------ 10 files changed, 89 insertions(+), 88 deletions(-) create mode 100644 src/processTargets/modifiers/scopeHandlers/scope.types.ts diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index c65b62dddb..f37472d3e9 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -8,7 +8,8 @@ import { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { getPreferredScope } from "./getPreferredScope"; import { runLegacy } from "./relativeScopeLegacy"; -import { ScopeHandler, TargetScope } from "./scopeHandlers/scopeHandler.types"; +import { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; +import { TargetScope } from "./scopeHandlers/scope.types"; import { TooFewScopesError } from "./TooFewScopesError"; export class RelativeScopeStage implements ModifierStage { diff --git a/src/processTargets/modifiers/constructScopeRangeTarget.ts b/src/processTargets/modifiers/constructScopeRangeTarget.ts index 786c62dd91..860588dff8 100644 --- a/src/processTargets/modifiers/constructScopeRangeTarget.ts +++ b/src/processTargets/modifiers/constructScopeRangeTarget.ts @@ -1,5 +1,5 @@ import { Target } from "../../typings/target.types"; -import { TargetScope } from "./scopeHandlers/scopeHandler.types"; +import { TargetScope } from "./scopeHandlers/scope.types"; export function constructScopeRangeTarget( isReversed: boolean, diff --git a/src/processTargets/modifiers/getPreferredScope.ts b/src/processTargets/modifiers/getPreferredScope.ts index bb349c86cb..7960204678 100644 --- a/src/processTargets/modifiers/getPreferredScope.ts +++ b/src/processTargets/modifiers/getPreferredScope.ts @@ -1,4 +1,4 @@ -import { Scope } from "./scopeHandlers/scopeHandler.types"; +import { Scope } from "./scopeHandlers/scope.types"; export function getPreferredScope(scopes: T[]): T | undefined { return getScopeHelper( diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index 01b51c41e8..f7285ea80e 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -6,10 +6,9 @@ import { Direction } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { LineTarget } from "../../targets"; import { - IterationScope, ScopeHandler, - TargetScope, } from "./scopeHandler.types"; +import { IterationScope, TargetScope } from "./scope.types"; export default class LineScopeHandler implements ScopeHandler { get scopeType(): ScopeType { diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index 497dfcc15b..a3187fa812 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -3,10 +3,9 @@ import { NoContainingScopeError } from "../../../errors"; import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; import { getPreferredScope } from "../getPreferredScope"; import { - IterationScope, ScopeHandler, - TargetScope, } from "./scopeHandler.types"; +import { IterationScope, TargetScope } from "./scope.types"; export default abstract class NestedScopeHandler implements ScopeHandler { constructor(private parentScopeHandler: ScopeHandler) {} diff --git a/src/processTargets/modifiers/scopeHandlers/SimpleRegexScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SimpleRegexScopeHandler.ts index adeac2604e..e2ad25acd0 100644 --- a/src/processTargets/modifiers/scopeHandlers/SimpleRegexScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/SimpleRegexScopeHandler.ts @@ -1,7 +1,8 @@ import { Range, TextEditor } from "vscode"; import { Target } from "../../../typings/target.types"; import NestedScopeHandler from "./NestedScopeHandler"; -import { ScopeHandler, TargetScope } from "./scopeHandler.types"; +import { ScopeHandler } from "./scopeHandler.types"; +import { TargetScope } from "./scope.types"; export default abstract class SimpleRegexScopeHandler extends NestedScopeHandler { constructor(parentScopeHandler: ScopeHandler, private regex: RegExp) { diff --git a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts index 4689d316b9..556ec6ac44 100644 --- a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts @@ -2,10 +2,9 @@ import { TextEditor, Position, Range } from "vscode"; import { ScopeType } from "../../../core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types"; import { Direction } from "../../../typings/targetDescriptor.types"; import { - IterationScope, ScopeHandler, - TargetScope, } from "./scopeHandler.types"; +import { IterationScope, TargetScope } from "./scope.types"; export default class SurroundingPairScopeHandler implements ScopeHandler { scopeType: ScopeType; diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index 918fd7fe0b..4b4ca846a1 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -5,7 +5,7 @@ import { getTokensInRange } from "../../../util/getTokensInRange"; import { TokenTarget } from "../../targets"; import LineScopeHandler from "./LineScopeHandler"; import NestedScopeHandler from "./NestedScopeHandler"; -import { TargetScope } from "./scopeHandler.types"; +import { TargetScope } from "./scope.types"; export default class TokenScopeHandler extends NestedScopeHandler { constructor() { diff --git a/src/processTargets/modifiers/scopeHandlers/scope.types.ts b/src/processTargets/modifiers/scopeHandlers/scope.types.ts new file mode 100644 index 0000000000..4ccc5a22ee --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/scope.types.ts @@ -0,0 +1,78 @@ +import { Range, TextEditor } from "vscode"; +import { Target } from "../../../typings/target.types"; + +/** + * A range in the document within which a particular scope type is considered + * to own the given region. We use this type both to define the domain within + * which a target is canonical, and the domain within which an iteration scope + * is canonical. + */ +export interface Scope { + /** + * The text editor containing {@link domain}. + */ + editor: TextEditor; + + /** + * The domain within which this scope is considered the canonical instance of + * this scope type. For example, if the scope type represents a `key` in a + * key-value pair, then the pair would be the `domain`, so that "take key" + * works from anywhere within the given pair. + * + * Most scopes will have a domain that is just the content range or removal + * range of the scope. + * + * For an iteration scope, indicates the domain within which this iteration scope is considered the canonical + * iteration scope for the given scope type. For example, if the scope type + * is function, then the domain might be a class, so that "take every funk" + * works from anywhere within the given class. + */ + domain: Range; + + /** + * This fuction can be defined to indicate how to choose between adjacent + * scopes. If the input target is zero width, and between two adjacent + * scopes, this funciton will be used to decide which scope is considered to + * contain the input target. If this function is `undefined`, or returns + * `undefined`, then the one to the right will be preferred. + * @param other The scope to compare to + */ + isPreferredOver?(other: Scope): boolean | undefined; +} + +/** + * Represents a scope, which is a specific instantiation of a scope type, + * eg a specific function, or a specific line or range of lines. Contains + * {@link target}, which represents the actual scope, as well as {@link domain}, + * which represents the range within which the given scope is canonical. For + * example, a scope representing the type of a parameter will have the entire + * parameter as its domain, so that one can say "take type" from anywhere + * within the parameter. + */ +export interface TargetScope extends Scope { + /** + * The target that represents this scope. Note that the target can represent + * a contiguous range of instances of the given scope type, eg a range from + * one function to another or a line range. + */ + getTarget(isReversed: boolean): Target; +} + +/** + * Represents an iteration scope, which is a domain containing one or more + * scopes that are considered siblings for use with `"every"`. This type + * contains {@link getScopes}, which is a list of the actual scopes, as well as + * {@link domain}, which represents the range within which the given iteration + * scope is canonical. For example, an iteration scope for the scope type + * `functionOrParameter` might have a class as its domain and its targets would + * be the functions in the class. This way one can say "take every funk" from + * anywhere within the class. + */ +export interface IterationScope extends Scope { + /** + * The scopes in the given iteration scope. Note that each scope has its own + * domain. We make this a function so that the scopes can be returned + * lazily. + */ + getScopes(): TargetScope[]; +} diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index c44d836bb1..5e46df1e79 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -1,83 +1,7 @@ import { Position, Range, TextEditor } from "vscode"; import { ScopeType } from "../../../core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types"; -import { Target } from "../../../typings/target.types"; import { Direction } from "../../../typings/targetDescriptor.types"; - -/** - * A range in the document within which a particular scope type is considered - * to own the given region. We use this type both to define the domain within - * which a target is canonical, and the domain within which an iteration scope - * is canonical. - */ -export interface Scope { - /** - * The text editor containing {@link domain}. - */ - editor: TextEditor; - - /** - * The domain within which this scope is considered the canonical instance of - * this scope type. For example, if the scope type represents a `key` in a - * key-value pair, then the pair would be the `domain`, so that "take key" - * works from anywhere within the given pair. - * - * Most scopes will have a domain that is just the content range or removal - * range of the scope. - * - * For an iteration scope, indicates the domain within which this iteration scope is considered the canonical - * iteration scope for the given scope type. For example, if the scope type - * is function, then the domain might be a class, so that "take every funk" - * works from anywhere within the given class. - */ - domain: Range; - - /** - * This fuction can be defined to indicate how to choose between adjacent - * scopes. If the input target is zero width, and between two adjacent - * scopes, this funciton will be used to decide which scope is considered to - * contain the input target. If this function is `undefined`, or returns - * `undefined`, then the one to the right will be preferred. - * @param other The scope to compare to - */ - isPreferredOver?(other: Scope): boolean | undefined; -} - -/** - * Represents a scope, which is a specific instantiation of a scope type, - * eg a specific function, or a specific line or range of lines. Contains - * {@link target}, which represents the actual scope, as well as {@link domain}, - * which represents the range within which the given scope is canonical. For - * example, a scope representing the type of a parameter will have the entire - * parameter as its domain, so that one can say "take type" from anywhere - * within the parameter. - */ -export interface TargetScope extends Scope { - /** - * The target that represents this scope. Note that the target can represent - * a contiguous range of instances of the given scope type, eg a range from - * one function to another or a line range. - */ - getTarget(isReversed: boolean): Target; -} - -/** - * Represents an iteration scope, which is a domain containing one or more - * scopes that are considered siblings for use with `"every"`. This type - * contains {@link getScopes}, which is a list of the actual scopes, as well as - * {@link domain}, which represents the range within which the given iteration - * scope is canonical. For example, an iteration scope for the scope type - * `functionOrParameter` might have a class as its domain and its targets would - * be the functions in the class. This way one can say "take every funk" from - * anywhere within the class. - */ -export interface IterationScope extends Scope { - /** - * The scopes in the given iteration scope. Note that each scope has its own - * domain. We make this a function so that the scopes can be returned - * lazily. - */ - getScopes(): TargetScope[]; -} +import { TargetScope, IterationScope } from "./scope.types"; /** * Represents a scope type. The functions in this interface allow us to find From 9513753c1dbc3a7f283cb61634702b3aa94f52e1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:54:55 +0000 Subject: [PATCH 23/69] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/processTargets/modifiers/ContainingScopeStage.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index 258c9c9739..1956f1e57f 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -51,10 +51,7 @@ export class ContainingScopeStage implements ModifierStage { const { scopeType } = this.modifier; const scopeHandler = getScopeHandler(scopeType); - const startScopes = scopeHandler.getScopesTouchingPosition( - editor, - start - ); + const startScopes = scopeHandler.getScopesTouchingPosition(editor, start); if (end.isEqual(start)) { return [getPreferredScope(startScopes).getTarget(isReversed)]; From e410ed54d7c7fac37d0a12a9edc8ac0a5567e8dd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:55:22 +0000 Subject: [PATCH 24/69] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../modifiers/scopeHandlers/LineScopeHandler.ts | 4 +--- .../modifiers/scopeHandlers/NestedScopeHandler.ts | 4 +--- .../modifiers/scopeHandlers/SurroundingPairScopeHandler.ts | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index f7285ea80e..21e9212e17 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -5,9 +5,7 @@ import { NoContainingScopeError } from "../../../errors"; import { Direction } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { LineTarget } from "../../targets"; -import { - ScopeHandler, -} from "./scopeHandler.types"; +import { ScopeHandler } from "./scopeHandler.types"; import { IterationScope, TargetScope } from "./scope.types"; export default class LineScopeHandler implements ScopeHandler { diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index a3187fa812..d0c9855e13 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -2,9 +2,7 @@ import { TextEditor, Position, Range } from "vscode"; import { NoContainingScopeError } from "../../../errors"; import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; import { getPreferredScope } from "../getPreferredScope"; -import { - ScopeHandler, -} from "./scopeHandler.types"; +import { ScopeHandler } from "./scopeHandler.types"; import { IterationScope, TargetScope } from "./scope.types"; export default abstract class NestedScopeHandler implements ScopeHandler { diff --git a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts index 556ec6ac44..fa7b4123f3 100644 --- a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts @@ -1,9 +1,7 @@ import { TextEditor, Position, Range } from "vscode"; import { ScopeType } from "../../../core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types"; import { Direction } from "../../../typings/targetDescriptor.types"; -import { - ScopeHandler, -} from "./scopeHandler.types"; +import { ScopeHandler } from "./scopeHandler.types"; import { IterationScope, TargetScope } from "./scope.types"; export default class SurroundingPairScopeHandler implements ScopeHandler { From 4b2325dbf3e9d7424b85c76f516edd57fef6632f Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 21:03:09 +0100 Subject: [PATCH 25/69] More jsdocs --- .../modifiers/scopeHandlers/scope.types.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/scope.types.ts b/src/processTargets/modifiers/scopeHandlers/scope.types.ts index 4ccc5a22ee..089b4f4be6 100644 --- a/src/processTargets/modifiers/scopeHandlers/scope.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scope.types.ts @@ -3,9 +3,9 @@ import { Target } from "../../../typings/target.types"; /** * A range in the document within which a particular scope type is considered - * to own the given region. We use this type both to define the domain within - * which a target is canonical, and the domain within which an iteration scope - * is canonical. + * the canonical instance of the given region. We use this type both to define + * the domain within which a target is canonical, and the domain within which + * an iteration scope is canonical. */ export interface Scope { /** @@ -43,17 +43,15 @@ export interface Scope { /** * Represents a scope, which is a specific instantiation of a scope type, * eg a specific function, or a specific line or range of lines. Contains - * {@link target}, which represents the actual scope, as well as {@link domain}, - * which represents the range within which the given scope is canonical. For - * example, a scope representing the type of a parameter will have the entire - * parameter as its domain, so that one can say "take type" from anywhere - * within the parameter. + * {@link getTarget}, which represents the actual scope, as well as + * {@link domain}, which represents the range within which the given scope is + * canonical. For example, a scope representing the type of a parameter will + * have the entire parameter as its domain, so that one can say "take type" + * from anywhere within the parameter. */ export interface TargetScope extends Scope { /** - * The target that represents this scope. Note that the target can represent - * a contiguous range of instances of the given scope type, eg a range from - * one function to another or a line range. + * The target corresponding to this scope. */ getTarget(isReversed: boolean): Target; } From e519f0d19d9f0e79676844020d4e828097773b47 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 21:05:19 +0100 Subject: [PATCH 26/69] Tweaks --- src/processTargets/modifiers/EveryScopeStage.ts | 2 +- .../modifiers/scopeHandlers/LineScopeHandler.ts | 7 ++++--- .../modifiers/scopeHandlers/NestedScopeHandler.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/processTargets/modifiers/EveryScopeStage.ts b/src/processTargets/modifiers/EveryScopeStage.ts index df6064f98d..99c0e85699 100644 --- a/src/processTargets/modifiers/EveryScopeStage.ts +++ b/src/processTargets/modifiers/EveryScopeStage.ts @@ -69,7 +69,7 @@ export class EveryScopeStage implements ModifierStage { ); } - return startScope.scopes.map((scope) => scope.getTarget(isReversed)); + return startScope.getScopes().map((scope) => scope.getTarget(isReversed)); } private runLegacy( diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index 21e9212e17..3081f9163e 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -37,9 +37,10 @@ export default class LineScopeHandler implements ScopeHandler { { editor, domain: getDocumentRange(editor.document), - scopes: range(editor.document.lineCount).map((lineNumber) => - lineNumberToScope(editor, lineNumber) - ), + getScopes: () => + range(editor.document.lineCount).map((lineNumber) => + lineNumberToScope(editor, lineNumber) + ), }, ]; } diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index d0c9855e13..8d743b4b88 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -51,7 +51,7 @@ export default abstract class NestedScopeHandler implements ScopeHandler { domain: parentScope.domain, editor, isPreferredOver: parentScope.isPreferredOver, - scopes: this.getScopesInParentScope(parentScope), + getScopes: () => this.getScopesInParentScope(parentScope), })); } From 39d6faf33c5bc8e3bd9f374fb2976f186bc33580 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 21:26:07 +0100 Subject: [PATCH 27/69] Test fixes --- .../modifiers/ContainingScopeStage.ts | 13 ++++++++-- .../modifiers/EveryScopeStage.ts | 22 +++++++++------- .../scopeHandlers/NestedScopeHandler.ts | 3 ++- .../SurroundingPairScopeHandler.ts | 26 ++++++++++--------- .../scopeHandlers/scopeHandler.types.ts | 2 +- .../containingScope/clearTwoTokens.yml | 6 ++--- .../clearTwoTokensBackward.yml | 6 ++--- .../relativeScopes/clearTwoToken3.yml | 6 +---- .../relativeScopes/clearTwoTokenBackward2.yml | 6 +---- 9 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index 1956f1e57f..8e9e3278b8 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -1,3 +1,4 @@ +import { NoContainingScopeError } from "../../errors"; import type { Target } from "../../typings/target.types"; import type { ContainingScopeModifier, @@ -53,11 +54,15 @@ export class ContainingScopeStage implements ModifierStage { const scopeHandler = getScopeHandler(scopeType); const startScopes = scopeHandler.getScopesTouchingPosition(editor, start); + if (startScopes.length === 0) { + throw new NoContainingScopeError(this.modifier.scopeType.type); + } + if (end.isEqual(start)) { - return [getPreferredScope(startScopes).getTarget(isReversed)]; + return [getPreferredScope(startScopes)!.getTarget(isReversed)]; } - const startScope = getRightScope(startScopes); + const startScope = getRightScope(startScopes)!; if (startScope.domain.contains(end)) { return [startScope.getTarget(isReversed)]; @@ -66,6 +71,10 @@ export class ContainingScopeStage implements ModifierStage { const endScopes = scopeHandler.getScopesTouchingPosition(editor, end); const endScope = getLeftScope(endScopes); + if (endScope == null) { + throw new NoContainingScopeError(this.modifier.scopeType.type); + } + return constructScopeRangeTarget(isReversed, startScope, endScope); } diff --git a/src/processTargets/modifiers/EveryScopeStage.ts b/src/processTargets/modifiers/EveryScopeStage.ts index 99c0e85699..0fda5c29fb 100644 --- a/src/processTargets/modifiers/EveryScopeStage.ts +++ b/src/processTargets/modifiers/EveryScopeStage.ts @@ -52,16 +52,18 @@ export class EveryScopeStage implements ModifierStage { const { start, end } = range; - const startScopes = scopeHandler.getIterationScopesTouchingPosition( - editor, - start - ); + const startIterationScopes = + scopeHandler.getIterationScopesTouchingPosition(editor, start); - const startScope = end.isEqual(start) - ? getPreferredScope(startScopes) - : getRightScope(startScopes); + const startIterationScope = end.isEqual(start) + ? getPreferredScope(startIterationScopes) + : getRightScope(startIterationScopes); - if (!startScope.domain.contains(end)) { + if (startIterationScope == null) { + throw new NoContainingScopeError(this.modifier.scopeType.type); + } + + if (!startIterationScope.domain.contains(end)) { // NB: This shouldn't really happen, because our weak scopes are // generally no bigger than a token. throw new Error( @@ -69,7 +71,9 @@ export class EveryScopeStage implements ModifierStage { ); } - return startScope.getScopes().map((scope) => scope.getTarget(isReversed)); + return startIterationScope + .getScopes() + .map((scope) => scope.getTarget(isReversed)); } private runLegacy( diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index 8d743b4b88..e173a677e0 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -4,6 +4,7 @@ import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; import { getPreferredScope } from "../getPreferredScope"; import { ScopeHandler } from "./scopeHandler.types"; import { IterationScope, TargetScope } from "./scope.types"; +import { OutOfRangeError } from "../targetSequenceUtils"; export default abstract class NestedScopeHandler implements ScopeHandler { constructor(private parentScopeHandler: ScopeHandler) {} @@ -74,7 +75,7 @@ export default abstract class NestedScopeHandler implements ScopeHandler { remainingOffset -= scopes.length; } - throw new NoContainingScopeError(this.scopeType.type); + throw new OutOfRangeError(); } private *iterateScopeGroups( diff --git a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts index fa7b4123f3..c41f506bec 100644 --- a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts @@ -1,39 +1,41 @@ import { TextEditor, Position, Range } from "vscode"; -import { ScopeType } from "../../../core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types"; -import { Direction } from "../../../typings/targetDescriptor.types"; +import { + Direction, + SurroundingPairScopeType, +} from "../../../typings/targetDescriptor.types"; import { ScopeHandler } from "./scopeHandler.types"; import { IterationScope, TargetScope } from "./scope.types"; export default class SurroundingPairScopeHandler implements ScopeHandler { - scopeType: ScopeType; + constructor(public readonly scopeType: SurroundingPairScopeType) {} getScopesTouchingPosition( - editor: TextEditor, - position: Position + _editor: TextEditor, + _position: Position ): TargetScope[] { // TODO: Run existing surrounding pair code on empty range constructed from // position, returning both if position is adjacent to to throw new Error("Method not implemented."); } - getScopesOverlappingRange(editor: TextEditor, range: Range): TargetScope[] { + getScopesOverlappingRange(_editor: TextEditor, _range: Range): TargetScope[] { // TODO: Implement https://github.com/cursorless-dev/cursorless/pull/1031#issuecomment-1276777449 throw new Error("Method not implemented."); } getIterationScopesTouchingPosition( - editor: TextEditor, - position: Position + _editor: TextEditor, + _position: Position ): IterationScope[] { // TODO: Return inside strict containing pair throw new Error("Method not implemented."); } getScopeRelativeToPosition( - editor: TextEditor, - position: Position, - offset: number, - direction: Direction + _editor: TextEditor, + _position: Position, + _offset: number, + _direction: Direction ): TargetScope { // TODO: Walk forward until we hit either an opening or closing delimiter. // If we hit an opening delimiter then we walk over as many pairs as we need diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 5e46df1e79..82521422bc 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -30,7 +30,7 @@ export interface ScopeHandler { /** * The scope type handled by this scope handler */ - scopeType: ScopeType; + readonly scopeType: ScopeType; /** * Return all scope(s) touching the given position. A scope is considered to diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml index 8eca8b9f64..0b26709ef3 100644 --- a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml @@ -19,8 +19,8 @@ initialState: active: {line: 0, character: 7} marks: {} finalState: - documentContents: aaa bbb + documentContents: aaa ccc selections: - - anchor: {line: 0, character: 7} - active: {line: 0, character: 7} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward.yml index 5fb9e859b4..0eed316fd9 100644 --- a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward.yml +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward.yml @@ -19,8 +19,8 @@ initialState: active: {line: 0, character: 7} marks: {} finalState: - documentContents: aaa ccc + documentContents: . ccc selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken3.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken3.yml index 4fad6e3887..a967aa51ab 100644 --- a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken3.yml +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken3.yml @@ -18,9 +18,5 @@ initialState: - anchor: {line: 0, character: 4} active: {line: 0, character: 4} marks: {} -finalState: - documentContents: "aaa " - selections: - - anchor: {line: 0, character: 5} - active: {line: 0, character: 5} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] +thrownError: {name: NoContainingScopeError} diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokenBackward2.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokenBackward2.yml index 072321a958..29312e843d 100644 --- a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokenBackward2.yml +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokenBackward2.yml @@ -18,9 +18,5 @@ initialState: - anchor: {line: 0, character: 8} active: {line: 0, character: 8} marks: {} -finalState: - documentContents: " ccc" - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] +thrownError: {name: NoContainingScopeError} From 5ff57d42c15ab8368d300ab11fa2bc517c0b8cf8 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 21:32:40 +0100 Subject: [PATCH 28/69] Fix error messages --- .../modifiers/scopeHandlers/LineScopeHandler.ts | 4 ++-- .../modifiers/scopeHandlers/NestedScopeHandler.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index 3081f9163e..f1af0403ab 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -1,12 +1,12 @@ import { range } from "lodash"; import { Position, Range, TextEditor } from "vscode"; import { ScopeType } from "../../../core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types"; -import { NoContainingScopeError } from "../../../errors"; import { Direction } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { LineTarget } from "../../targets"; import { ScopeHandler } from "./scopeHandler.types"; import { IterationScope, TargetScope } from "./scope.types"; +import { OutOfRangeError } from "../targetSequenceUtils"; export default class LineScopeHandler implements ScopeHandler { get scopeType(): ScopeType { @@ -55,7 +55,7 @@ export default class LineScopeHandler implements ScopeHandler { direction === "forward" ? position.line + offset : position.line - offset; if (lineNumber < 0 || lineNumber >= editor.document.lineCount) { - throw new NoContainingScopeError(this.scopeType.type); + throw new OutOfRangeError(); } return lineNumberToScope(editor, lineNumber); diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index e173a677e0..0f91e8b628 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -1,5 +1,4 @@ import { TextEditor, Position, Range } from "vscode"; -import { NoContainingScopeError } from "../../../errors"; import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; import { getPreferredScope } from "../getPreferredScope"; import { ScopeHandler } from "./scopeHandler.types"; From 5aeb2a62111ea61ea08c746ea45541527c2c906e Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 21:42:34 +0100 Subject: [PATCH 29/69] Revert `OrdinalScopeStage` --- src/processTargets/modifiers/OrdinalScopeStage.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/processTargets/modifiers/OrdinalScopeStage.ts b/src/processTargets/modifiers/OrdinalScopeStage.ts index 9e80dd0427..8ea114e82e 100644 --- a/src/processTargets/modifiers/OrdinalScopeStage.ts +++ b/src/processTargets/modifiers/OrdinalScopeStage.ts @@ -17,13 +17,6 @@ export class OrdinalScopeStage implements ModifierStage { this.modifier.scopeType ); - return this.calculateIndicesAndCreateTarget(target, targets); - } - - private calculateIndicesAndCreateTarget( - target: Target, - targets: Target[] - ): Target[] { const startIndex = this.modifier.start + (this.modifier.start < 0 ? targets.length : 0); const endIndex = startIndex + this.modifier.length - 1; From ea80465b6e44e230fb2b156268059aab9b85bf9a Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Oct 2022 21:47:35 +0100 Subject: [PATCH 30/69] Tweak --- src/processTargets/modifiers/ContainingScopeStage.ts | 2 +- src/processTargets/modifiers/RelativeScopeStage.ts | 4 ++-- .../modifiers/constructScopeRangeTarget.ts | 11 +++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index 8e9e3278b8..bf56446859 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -75,7 +75,7 @@ export class ContainingScopeStage implements ModifierStage { throw new NoContainingScopeError(this.modifier.scopeType.type); } - return constructScopeRangeTarget(isReversed, startScope, endScope); + return [constructScopeRangeTarget(isReversed, startScope, endScope)]; } private runLegacy( diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index f37472d3e9..0c8254e01c 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -65,7 +65,7 @@ export class RelativeScopeStage implements ModifierStage { direction ); - return constructScopeRangeTarget(isReversed, proximalScope, distalScope); + return [constructScopeRangeTarget(isReversed, proximalScope, distalScope)]; } private handleIncludingIntersecting(target: Target): Target[] { @@ -105,7 +105,7 @@ export class RelativeScopeStage implements ModifierStage { ? index0Scopes.at(-1)! : index0Scopes[0]; - return constructScopeRangeTarget(isReversed, proximalScope, distalScope); + return [constructScopeRangeTarget(isReversed, proximalScope, distalScope)]; } } diff --git a/src/processTargets/modifiers/constructScopeRangeTarget.ts b/src/processTargets/modifiers/constructScopeRangeTarget.ts index 860588dff8..84a97bff35 100644 --- a/src/processTargets/modifiers/constructScopeRangeTarget.ts +++ b/src/processTargets/modifiers/constructScopeRangeTarget.ts @@ -5,7 +5,7 @@ export function constructScopeRangeTarget( isReversed: boolean, scope1: TargetScope, scope2: TargetScope -): Target[] { +): Target { const target1 = scope1.getTarget(isReversed); const target2 = scope2.getTarget(isReversed); @@ -17,7 +17,10 @@ export function constructScopeRangeTarget( ? [target1, target2] : [target2, target1]; - return [ - startTarget.createContinuousRangeTarget(isReversed, endTarget, true, true), - ]; + return startTarget.createContinuousRangeTarget( + isReversed, + endTarget, + true, + true + ); } From 234811add6234e1dcf1535b0ceb5b20c80a2a3c9 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 09:06:13 +0100 Subject: [PATCH 31/69] jsdocs; fix import --- .../modifiers/scopeHandlers/scope.types.ts | 4 +- .../scopeHandlers/scopeHandler.types.ts | 42 ++++++++++--------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/scope.types.ts b/src/processTargets/modifiers/scopeHandlers/scope.types.ts index 089b4f4be6..e7251887b3 100644 --- a/src/processTargets/modifiers/scopeHandlers/scope.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scope.types.ts @@ -11,7 +11,7 @@ export interface Scope { /** * The text editor containing {@link domain}. */ - editor: TextEditor; + readonly editor: TextEditor; /** * The domain within which this scope is considered the canonical instance of @@ -27,7 +27,7 @@ export interface Scope { * is function, then the domain might be a class, so that "take every funk" * works from anywhere within the given class. */ - domain: Range; + readonly domain: Range; /** * This fuction can be defined to indicate how to choose between adjacent diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 82521422bc..60476e6d43 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -1,27 +1,28 @@ import { Position, Range, TextEditor } from "vscode"; -import { ScopeType } from "../../../core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types"; -import { Direction } from "../../../typings/targetDescriptor.types"; +import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; import { TargetScope, IterationScope } from "./scope.types"; /** * Represents a scope type. The functions in this interface allow us to find - * specific instances of the given scope type in a document. For example, it - * has a function to find the scopes touching a given position, a function to - * find every instance of the scope overlapping a range, etc. These functions - * are used by the various modifier stages to implement modifiers that involve - * the given scope type. + * specific instances of the given scope type in a document. For example, there + * is a function to find the scopes touching a given position + * ({@link getScopesTouchingPosition}), a function to find every instance of + * the scope overlapping a range ({@link getScopesOverlappingRange}), etc. + * These functions are used by the various modifier stages to implement + * modifiers that involve the given scope type, such as containing, every, + * next, etc. * - * Note that a scope type can be hierarchical, ie one scope of the given type + * Note that some scope types are hierarchical, ie one scope of the given type * can contain another scope of the same type. For example, a function can * contain other functions, so functions are hierarchical. Surrounding pairs * are also hierarchical, as they can be nested. Many scope types are not * hierarchical, though, eg line, token, word, etc. * * In the case of a hierarchical scope type, these functions should never - * return scopes that contain one another. Ie if we return a surrounding pair, - * we shouldn't also return any surrounding pairs contained within, or if we - * return a function, we shouldn't also return a function nested within that - * function. + * return two scopes that contain one another. Ie if we return a surrounding + * pair, we shouldn't also return any surrounding pairs contained within, or + * if we return a function, we shouldn't also return a function nested within + * that function. * * Note that there are helpers that can sometimes be used to avoid implementing * a scope handler from scratch, eg {@link NestedScopeHandler}. @@ -77,10 +78,10 @@ export interface ScopeHandler { /** * Returns all iteration scopes touching {@link position}. For example, if * scope type is `namedFunction`, and {@link position} is inside a class, the - * iteration scope would contain a list of functions in the class. A scope - * is considered to touch a position if its domain contains the position or - * is directly adjacent to the position. In other words, return all iteration - * scopes for which the following is true: + * iteration scope would contain a list of functions in the class. An + * iteration scope is considered to touch a position if its domain contains + * the position or is directly adjacent to the position. In other words, + * return all iteration scopes for which the following is true: * * ```typescript * iterationScope.domain.start <= position && iterationScope.domain.end >= position @@ -92,9 +93,9 @@ export interface ScopeHandler { * position, return an empty list. * * Note that if the iteration scope type is hierarchical, return only minimal - * scopes, ie if iteration scope A and iteration scope B both touch - * {@link position}, and iteration scope A contains iteration scope B, return - * iteration scope B but not iteration scope A. + * iteration scopes, ie if iteration scope A and iteration scope B both touch + * {@link position}, and iteration scope A contains iteration scope B, + * return iteration scope B but not iteration scope A. * * @param editor The editor containing {@link position} * @param position The position from which to expand @@ -112,7 +113,8 @@ export interface ScopeHandler { * scopes whose {@link Scope.domain.end} is equal or before * {@link position}. Note that {@link offset} will always be greater than or * equal to 1. For example, an {@link offset} of 1 should return the first - * scope after {@link position} (before if {@link direction} is `"backward"`) + * scope after {@link position} (before if {@link direction} is + * `"backward"`). * @param editor The editor containing {@link position} * @param position The position from which to start * @param offset Which scope before / after position to return From 969bc720e98b89a7526656622b40375285b9c5f7 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 09:11:03 +0100 Subject: [PATCH 32/69] Don't export legacy types --- .../upgradeV2ToV3/targetDescriptorV2.types.ts | 86 ++++--------------- .../upgradeV2ToV3/upgradeV2ToV3.ts | 8 +- 2 files changed, 23 insertions(+), 71 deletions(-) diff --git a/src/core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types.ts b/src/core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types.ts index 2a62f6bf4c..de3cb001ba 100644 --- a/src/core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types.ts +++ b/src/core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types.ts @@ -145,37 +145,32 @@ export interface SurroundingPairScopeType { requireStrongContainment?: boolean; } -export type ScopeType = +export type ScopeTypeV2 = | SimpleScopeType | SurroundingPairScopeType | CustomRegexScopeType; -export interface ContainingSurroundingPairModifier - extends ContainingScopeModifier { - scopeType: SurroundingPairScopeType; -} - -export interface InteriorOnlyModifier { +interface InteriorOnlyModifier { type: "interiorOnly"; } -export interface ExcludeInteriorModifier { +interface ExcludeInteriorModifier { type: "excludeInterior"; } -export interface ContainingScopeModifier { +interface ContainingScopeModifier { type: "containingScope"; - scopeType: ScopeType; + scopeType: ScopeTypeV2; } -export interface EveryScopeModifier { +interface EveryScopeModifier { type: "everyScope"; - scopeType: ScopeType; + scopeType: ScopeTypeV2; } -export interface OrdinalRangeModifier { +export interface OrdinalRangeModifierV2 { type: "ordinalRange"; - scopeType: ScopeType; + scopeType: ScopeTypeV2; anchor: number; active: number; excludeAnchor?: boolean; @@ -187,21 +182,21 @@ export interface OrdinalRangeModifier { * example if it is the destination of a bring or move it should inherit the * type information such as delimiters from its source. */ -export interface RawSelectionModifier { +interface RawSelectionModifier { type: "toRawSelection"; } -export interface LeadingModifier { +interface LeadingModifier { type: "leading"; } -export interface TrailingModifier { +interface TrailingModifier { type: "trailing"; } -export type Position = "before" | "after" | "start" | "end"; +type Position = "before" | "after" | "start" | "end"; -export interface PositionModifier { +interface PositionModifier { type: "position"; position: Position; } @@ -213,7 +208,7 @@ export interface PartialPrimitiveTargetDescriptorV2 { isImplicit?: boolean; } -export interface HeadTailModifier { +interface HeadTailModifier { type: "extendThroughStartOf" | "extendThroughEndOf"; modifiers?: ModifierV2[]; } @@ -222,7 +217,7 @@ export interface HeadTailModifier { * Runs {@link modifier} if the target has no explicit scope type, ie if * {@link Target.hasExplicitScopeType} is `false`. */ -export interface ModifyIfUntypedModifier { +interface ModifyIfUntypedModifier { type: "modifyIfUntyped"; /** @@ -236,7 +231,7 @@ export interface ModifyIfUntypedModifier { * doesn't throw an error, returning the output from the first modifier not * throwing an error. */ -export interface CascadingModifier { +interface CascadingModifier { type: "cascading"; /** @@ -251,7 +246,7 @@ export type ModifierV2 = | ExcludeInteriorModifier | ContainingScopeModifier | EveryScopeModifier - | OrdinalRangeModifier + | OrdinalRangeModifierV2 | HeadTailModifier | LeadingModifier | TrailingModifier @@ -281,50 +276,7 @@ export type PartialTargetDescriptorV2 = | PartialRangeTargetDescriptorV2 | PartialListTargetDescriptorV2; -export interface PrimitiveTargetDescriptor - extends PartialPrimitiveTargetDescriptorV2 { - /** - * The mark, eg "air", "this", "that", etc - */ - mark: MarkV2; - - /** - * Zero or more modifiers that will be applied in sequence to the output from - * the mark. Note that the modifiers will be applied in reverse order. For - * example, if the user says "take first char name air", then we will apply - * "name" to the output of "air" to select the name of the function or - * statement containing "air", then apply "first char" to select the first - * character of the name. - */ - modifiers: ModifierV2[]; - - /** - * We separate the positional modifier from the other modifiers because it - * behaves differently and and makes the target behave like a destination for - * example for bring. This change is the first step toward #803 - */ - positionModifier?: PositionModifier; -} - -export interface RangeTargetDescriptor { - type: "range"; - anchor: PrimitiveTargetDescriptor; - active: PrimitiveTargetDescriptor; - excludeAnchor: boolean; - excludeActive: boolean; - rangeType: RangeType; -} // continuous is one single continuous selection between the two targets // vertical puts a selection on each line vertically between the two targets -export type RangeType = "continuous" | "vertical"; - -export interface ListTargetDescriptor { - type: "list"; - elements: (PrimitiveTargetDescriptor | RangeTargetDescriptor)[]; -} - -export type TargetDescriptor = - | PrimitiveTargetDescriptor - | RangeTargetDescriptor - | ListTargetDescriptor; +type RangeType = "continuous" | "vertical"; diff --git a/src/core/commandVersionUpgrades/upgradeV2ToV3/upgradeV2ToV3.ts b/src/core/commandVersionUpgrades/upgradeV2ToV3/upgradeV2ToV3.ts index 619763c86e..8f4bc0790d 100644 --- a/src/core/commandVersionUpgrades/upgradeV2ToV3/upgradeV2ToV3.ts +++ b/src/core/commandVersionUpgrades/upgradeV2ToV3/upgradeV2ToV3.ts @@ -17,10 +17,10 @@ import { LineNumberPositionV2, MarkV2, ModifierV2, - OrdinalRangeModifier, + OrdinalRangeModifierV2, PartialPrimitiveTargetDescriptorV2, PartialTargetDescriptorV2, - ScopeType, + ScopeTypeV2, } from "./targetDescriptorV2.types"; export function upgradeV2ToV3(command: CommandV2): CommandV3 { @@ -104,7 +104,7 @@ function createLineNumberMark( } function createOrdinalModifier( - modifier: OrdinalRangeModifier + modifier: OrdinalRangeModifierV2 ): OrdinalScopeModifier | RangeModifier { if (modifier.anchor === modifier.active) { return createAbsoluteOrdinalModifier(modifier.scopeType, modifier.anchor); @@ -130,7 +130,7 @@ function createLineNumberMarkFromPos( } function createAbsoluteOrdinalModifier( - scopeType: ScopeType, + scopeType: ScopeTypeV2, start: number ): OrdinalScopeModifier { return { From 084824c1969fa4d7917df356a9e72b021a202c0d Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 09:11:07 +0100 Subject: [PATCH 33/69] Fix import --- src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index f1af0403ab..263172dbbf 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -1,7 +1,6 @@ import { range } from "lodash"; import { Position, Range, TextEditor } from "vscode"; -import { ScopeType } from "../../../core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types"; -import { Direction } from "../../../typings/targetDescriptor.types"; +import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { LineTarget } from "../../targets"; import { ScopeHandler } from "./scopeHandler.types"; From b483468c07fdd56933564a7f6071e0264e3abf9b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 11:51:03 +0100 Subject: [PATCH 34/69] Preparation for surrounding pairs --- .../modifiers/RelativeScopeStage.ts | 43 +++++++++++++++---- src/util/rangeUtils.ts | 4 ++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index 0c8254e01c..c90796cda0 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -11,6 +11,7 @@ import { runLegacy } from "./relativeScopeLegacy"; import { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; import { TargetScope } from "./scopeHandlers/scope.types"; import { TooFewScopesError } from "./TooFewScopesError"; +import { strictlyContains } from "../../util/rangeUtils"; export class RelativeScopeStage implements ModifierStage { constructor(private modifier: RelativeScopeModifier) {} @@ -38,12 +39,38 @@ export class RelativeScopeStage implements ModifierStage { const index0Scopes = getIndex0Scopes(scopeHandler, editor, range); - const initialPosition = - index0Scopes.length > 0 - ? getIndex0DistalPosition(direction, index0Scopes) - : direction === "forward" - ? range.end - : range.start; + /** + * Indicates whether we move our initial position past the index 0 scopes + * before finding next scopes. The two cases we need to keep in mind are as + * follows: + * + * - For surrounding pairs, eg "round", we don't want to skip past the + * containing pair if we're in the middle of a pair. For example, in + * `(() | ())`, where `|` is our cursor, we want `"next round"` to select the + * second `()`, rather than jumping out of the big pair. + * - For tokens, we want to take into account what is considered the + * containing scope so that `"next token"` and `"token"` don't just do the + * same thing. For example, in `foo|. bar`, where `|` is the cursor, + * `"token"` is `foo`, so we want `"next token"` to refer to `.`, but in + * `.|foo bar`, `"token"` is also `foo`, so `"next token"` should refer to + * `bar`. + * + * To accommodate both of these cases, if we have only one index 0 scope, + * we ignore it if it completely contains {@link range}. That way when the + * token adjacency logic kicks in, we'll respect it, but if we're in the + * middle of a "round", we won't skip to the end. + * + */ + const skipIndex0Scopes = + index0Scopes.length > 1 || + (index0Scopes.length === 1 && + !strictlyContains(index0Scopes[0].domain, range)); + + const initialPosition = skipIndex0Scopes + ? getIndex0DistalPosition(direction, index0Scopes) + : direction === "forward" + ? range.end + : range.start; const proximalScope = scopeHandler.getScopeRelativeToPosition( editor, @@ -128,8 +155,8 @@ function getIndex0DistalPosition( /** * Returns a list of scopes that are considered to be at relative scope index * 0, ie "containing" / "intersecting" with the input target. If the input - * target is zero length, we return the containing scope, otherwise we return - * the intersecting scopes. + * target is zero length, we return at most one scope: the same scope preferred + * by {@link ContainingScopeModifier}. * @param scopeHandler The scope handler for the given scope type * @param editor The editor containing {@link range} * @param range The input target range diff --git a/src/util/rangeUtils.ts b/src/util/rangeUtils.ts index 0d2dd02712..37ac0cbc36 100644 --- a/src/util/rangeUtils.ts +++ b/src/util/rangeUtils.ts @@ -35,3 +35,7 @@ export function getRangeLength(editor: TextEditor, range: Range) { : editor.document.offsetAt(range.end) - editor.document.offsetAt(range.start); } + +export function strictlyContains(range1: Range, range2: Range): boolean { + return range1.start.isBefore(range2.start) && range1.end.isAfter(range2.end); +} From 5ef5c412f342f7d64c984b9d345a51eaec62d210 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 11:55:51 +0100 Subject: [PATCH 35/69] Naming cleanup --- src/processTargets/modifiers/RelativeScopeStage.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index c90796cda0..4644d53e6b 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -33,7 +33,12 @@ export class RelativeScopeStage implements ModifierStage { private handleNotIncludingIntersecting(target: Target): Target[] { const { isReversed, editor, contentRange: range } = target; - const { scopeType, length, direction, offset } = this.modifier; + const { + scopeType, + length: desiredScopeCount, + direction, + offset, + } = this.modifier; const scopeHandler = getScopeHandler(scopeType); @@ -79,7 +84,7 @@ export class RelativeScopeStage implements ModifierStage { direction ); - if (length === 1) { + if (desiredScopeCount === 1) { return [proximalScope.getTarget(isReversed)]; } @@ -88,7 +93,7 @@ export class RelativeScopeStage implements ModifierStage { direction === "forward" ? proximalScope.domain.end : proximalScope.domain.start, - length - 1, + desiredScopeCount - 1, direction ); From e73c420cc9c46fade57f69e4f075e4b0a5600fd5 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 12:08:21 +0100 Subject: [PATCH 36/69] Unify `getLegacyScopeStage` functions --- .../modifiers/ContainingScopeStage.ts | 61 +------------------ .../modifiers/EveryScopeStage.ts | 51 +--------------- .../modifiers/SurroundingPairStage.ts | 11 +++- .../modifiers/getLegacyScopeStage.ts | 57 +++++++++++++++++ src/typings/targetDescriptor.types.ts | 8 +++ 5 files changed, 79 insertions(+), 109 deletions(-) create mode 100644 src/processTargets/modifiers/getLegacyScopeStage.ts diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index bf56446859..69e10b9937 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -1,34 +1,16 @@ import { NoContainingScopeError } from "../../errors"; import type { Target } from "../../typings/target.types"; -import type { - ContainingScopeModifier, - ContainingSurroundingPairModifier, -} from "../../typings/targetDescriptor.types"; +import type { ContainingScopeModifier } from "../../typings/targetDescriptor.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; import getScopeHandler from "../getScopeHandler"; import type { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import getLegacyScopeStage from "./getLegacyScopeStage"; import { getLeftScope, getPreferredScope, getRightScope, } from "./getPreferredScope"; -import ItemStage from "./ItemStage"; -import BoundedNonWhitespaceSequenceStage from "./scopeTypeStages/BoundedNonWhitespaceStage"; -import ContainingSyntaxScopeStage, { - SimpleContainingScopeModifier, -} from "./scopeTypeStages/ContainingSyntaxScopeStage"; -import DocumentStage from "./scopeTypeStages/DocumentStage"; -import NotebookCellStage from "./scopeTypeStages/NotebookCellStage"; -import ParagraphStage from "./scopeTypeStages/ParagraphStage"; -import { - CustomRegexModifier, - CustomRegexStage, - NonWhitespaceSequenceStage, - UrlStage, -} from "./scopeTypeStages/RegexStage"; -import { CharacterStage, WordStage } from "./scopeTypeStages/SubTokenStages"; -import SurroundingPairStage from "./SurroundingPairStage"; export class ContainingScopeStage implements ModifierStage { constructor(private modifier: ContainingScopeModifier) {} @@ -82,43 +64,6 @@ export class ContainingScopeStage implements ModifierStage { context: ProcessedTargetsContext, target: Target ): Target[] { - const legacyStage = getContainingScopeStage(this.modifier); - return legacyStage.run(context, target); + return getLegacyScopeStage(this.modifier).run(context, target); } } - -const getContainingScopeStage = ( - modifier: ContainingScopeModifier -): ModifierStage => { - switch (modifier.scopeType.type) { - case "notebookCell": - return new NotebookCellStage(modifier); - case "document": - return new DocumentStage(modifier); - case "paragraph": - return new ParagraphStage(modifier); - case "nonWhitespaceSequence": - return new NonWhitespaceSequenceStage(modifier); - case "boundedNonWhitespaceSequence": - return new BoundedNonWhitespaceSequenceStage(modifier); - case "url": - return new UrlStage(modifier); - case "collectionItem": - return new ItemStage(modifier); - case "customRegex": - return new CustomRegexStage(modifier as CustomRegexModifier); - case "word": - return new WordStage(modifier); - case "character": - return new CharacterStage(modifier); - case "surroundingPair": - return new SurroundingPairStage( - modifier as ContainingSurroundingPairModifier - ); - default: - // Default to containing syntax scope using tree sitter - return new ContainingSyntaxScopeStage( - modifier as SimpleContainingScopeModifier - ); - } -}; diff --git a/src/processTargets/modifiers/EveryScopeStage.ts b/src/processTargets/modifiers/EveryScopeStage.ts index 0fda5c29fb..f5496d0396 100644 --- a/src/processTargets/modifiers/EveryScopeStage.ts +++ b/src/processTargets/modifiers/EveryScopeStage.ts @@ -4,22 +4,8 @@ import type { EveryScopeModifier } from "../../typings/targetDescriptor.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; import getScopeHandler from "../getScopeHandler"; import type { ModifierStage } from "../PipelineStages.types"; +import getLegacyScopeStage from "./getLegacyScopeStage"; import { getPreferredScope, getRightScope } from "./getPreferredScope"; -import ItemStage from "./ItemStage"; -import BoundedNonWhitespaceSequenceStage from "./scopeTypeStages/BoundedNonWhitespaceStage"; -import ContainingSyntaxScopeStage, { - SimpleEveryScopeModifier, -} from "./scopeTypeStages/ContainingSyntaxScopeStage"; -import DocumentStage from "./scopeTypeStages/DocumentStage"; -import NotebookCellStage from "./scopeTypeStages/NotebookCellStage"; -import ParagraphStage from "./scopeTypeStages/ParagraphStage"; -import { - CustomRegexModifier, - CustomRegexStage, - NonWhitespaceSequenceStage, - UrlStage, -} from "./scopeTypeStages/RegexStage"; -import { CharacterStage, WordStage } from "./scopeTypeStages/SubTokenStages"; export class EveryScopeStage implements ModifierStage { constructor(private modifier: EveryScopeModifier) {} @@ -80,39 +66,6 @@ export class EveryScopeStage implements ModifierStage { context: ProcessedTargetsContext, target: Target ): Target[] { - const legacyStage = getEveryScopeStage(this.modifier); - return legacyStage.run(context, target); + return getLegacyScopeStage(this.modifier).run(context, target); } } - -const getEveryScopeStage = (modifier: EveryScopeModifier): ModifierStage => { - switch (modifier.scopeType.type) { - case "notebookCell": - return new NotebookCellStage(modifier); - case "document": - return new DocumentStage(modifier); - case "paragraph": - return new ParagraphStage(modifier); - case "nonWhitespaceSequence": - return new NonWhitespaceSequenceStage(modifier); - case "boundedNonWhitespaceSequence": - return new BoundedNonWhitespaceSequenceStage(modifier); - case "url": - return new UrlStage(modifier); - case "collectionItem": - return new ItemStage(modifier); - case "customRegex": - return new CustomRegexStage(modifier as CustomRegexModifier); - case "word": - return new WordStage(modifier); - case "character": - return new CharacterStage(modifier); - case "surroundingPair": - throw Error(`Unsupported every scope ${modifier.scopeType.type}`); - default: - // Default to containing syntax scope using tree sitter - return new ContainingSyntaxScopeStage( - modifier as SimpleEveryScopeModifier - ); - } -}; diff --git a/src/processTargets/modifiers/SurroundingPairStage.ts b/src/processTargets/modifiers/SurroundingPairStage.ts index e387b981b0..da6c1b0664 100644 --- a/src/processTargets/modifiers/SurroundingPairStage.ts +++ b/src/processTargets/modifiers/SurroundingPairStage.ts @@ -1,5 +1,8 @@ import type { Target } from "../../typings/target.types"; -import type { ContainingSurroundingPairModifier } from "../../typings/targetDescriptor.types"; +import type { + ContainingSurroundingPairModifier, + SurroundingPairModifier, +} from "../../typings/targetDescriptor.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; import type { ModifierStage } from "../PipelineStages.types"; import { SurroundingPairTarget } from "../targets"; @@ -19,12 +22,16 @@ import { processSurroundingPair } from "./surroundingPair"; * `null` if none was found */ export default class SurroundingPairStage implements ModifierStage { - constructor(private modifier: ContainingSurroundingPairModifier) {} + constructor(private modifier: SurroundingPairModifier) {} run( context: ProcessedTargetsContext, target: Target ): SurroundingPairTarget[] { + if (this.modifier.type === "everyScope") { + throw Error(`Unsupported every scope ${this.modifier.scopeType.type}`); + } + return processedSurroundingPairTarget(this.modifier, context, target); } } diff --git a/src/processTargets/modifiers/getLegacyScopeStage.ts b/src/processTargets/modifiers/getLegacyScopeStage.ts new file mode 100644 index 0000000000..ed34b7ecd6 --- /dev/null +++ b/src/processTargets/modifiers/getLegacyScopeStage.ts @@ -0,0 +1,57 @@ +import { + ContainingScopeModifier, + EveryScopeModifier, + SurroundingPairModifier, +} from "../../typings/targetDescriptor.types"; +import { ModifierStage } from "../PipelineStages.types"; +import ItemStage from "./ItemStage"; +import BoundedNonWhitespaceSequenceStage from "./scopeTypeStages/BoundedNonWhitespaceStage"; +import ContainingSyntaxScopeStage, { + SimpleContainingScopeModifier, + SimpleEveryScopeModifier, +} from "./scopeTypeStages/ContainingSyntaxScopeStage"; +import DocumentStage from "./scopeTypeStages/DocumentStage"; +import NotebookCellStage from "./scopeTypeStages/NotebookCellStage"; +import ParagraphStage from "./scopeTypeStages/ParagraphStage"; +import { + CustomRegexModifier, + CustomRegexStage, + NonWhitespaceSequenceStage, + UrlStage, +} from "./scopeTypeStages/RegexStage"; +import { CharacterStage, WordStage } from "./scopeTypeStages/SubTokenStages"; +import SurroundingPairStage from "./SurroundingPairStage"; + +export default function getLegacyScopeStage( + modifier: ContainingScopeModifier | EveryScopeModifier +): ModifierStage { + switch (modifier.scopeType.type) { + case "notebookCell": + return new NotebookCellStage(modifier); + case "document": + return new DocumentStage(modifier); + case "paragraph": + return new ParagraphStage(modifier); + case "nonWhitespaceSequence": + return new NonWhitespaceSequenceStage(modifier); + case "boundedNonWhitespaceSequence": + return new BoundedNonWhitespaceSequenceStage(modifier); + case "url": + return new UrlStage(modifier); + case "collectionItem": + return new ItemStage(modifier); + case "customRegex": + return new CustomRegexStage(modifier as CustomRegexModifier); + case "word": + return new WordStage(modifier); + case "character": + return new CharacterStage(modifier); + case "surroundingPair": + return new SurroundingPairStage(modifier as SurroundingPairModifier); + default: + // Default to containing syntax scope using tree sitter + return new ContainingSyntaxScopeStage( + modifier as SimpleContainingScopeModifier | SimpleEveryScopeModifier + ); + } +} diff --git a/src/typings/targetDescriptor.types.ts b/src/typings/targetDescriptor.types.ts index b6287a66ff..52755635d6 100644 --- a/src/typings/targetDescriptor.types.ts +++ b/src/typings/targetDescriptor.types.ts @@ -164,6 +164,14 @@ export interface ContainingSurroundingPairModifier scopeType: SurroundingPairScopeType; } +export interface EverySurroundingPairModifier extends EveryScopeModifier { + scopeType: SurroundingPairScopeType; +} + +export type SurroundingPairModifier = + | ContainingSurroundingPairModifier + | EverySurroundingPairModifier; + export interface InteriorOnlyModifier { type: "interiorOnly"; } From 0628ca66465b53815b06253011fc1c31d243a4a7 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 16:19:55 +0100 Subject: [PATCH 37/69] Lots of cleanup --- src/processTargets/getScopeHandler.ts | 15 ---- src/processTargets/marks/LineNumberStage.ts | 2 +- .../modifiers/ContainingScopeStage.ts | 29 +++---- .../modifiers/EveryScopeStage.ts | 28 ++----- .../modifiers/ItemStage/getIterationScope.ts | 2 +- .../modifiers/RelativeScopeStage.ts | 41 +++++---- .../scopeHandlers/LineScopeHandler.ts | 12 ++- .../scopeHandlers/NestedScopeHandler.ts | 83 +++++++++++-------- .../scopeHandlers/SimpleRegexScopeHandler.ts | 42 ---------- .../SurroundingPairScopeHandler.ts | 9 +- .../scopeHandlers/TokenScopeHandler.ts | 15 ++-- .../scopeHandlers/getScopeHandler.ts | 27 ++++++ .../modifiers/scopeHandlers/index.ts | 8 ++ .../scopeHandlers/scopeHandler.types.ts | 6 ++ .../scopeTypeStages/ParagraphStage.ts | 2 +- src/util/regex.ts | 19 +++++ 16 files changed, 176 insertions(+), 164 deletions(-) delete mode 100644 src/processTargets/getScopeHandler.ts delete mode 100644 src/processTargets/modifiers/scopeHandlers/SimpleRegexScopeHandler.ts create mode 100644 src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts create mode 100644 src/processTargets/modifiers/scopeHandlers/index.ts diff --git a/src/processTargets/getScopeHandler.ts b/src/processTargets/getScopeHandler.ts deleted file mode 100644 index f01222850e..0000000000 --- a/src/processTargets/getScopeHandler.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ScopeType } from "../typings/targetDescriptor.types"; -import LineScopeHandler from "./modifiers/scopeHandlers/LineScopeHandler"; -import { ScopeHandler } from "./modifiers/scopeHandlers/scopeHandler.types"; -import TokenScopeHandler from "./modifiers/scopeHandlers/TokenScopeHandler"; - -export default (scopeType: ScopeType): ScopeHandler => { - switch (scopeType.type) { - case "token": - return new TokenScopeHandler(); - case "line": - return new LineScopeHandler(); - default: - throw Error(`Unknown scope handler ${scopeType.type}`); - } -}; diff --git a/src/processTargets/marks/LineNumberStage.ts b/src/processTargets/marks/LineNumberStage.ts index 5f063a0e99..45fb03dcd2 100644 --- a/src/processTargets/marks/LineNumberStage.ts +++ b/src/processTargets/marks/LineNumberStage.ts @@ -4,7 +4,7 @@ import type { LineNumberType, } from "../../typings/targetDescriptor.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; -import { createLineTarget } from "../modifiers/scopeHandlers/LineScopeHandler"; +import { createLineTarget } from "../modifiers/scopeHandlers"; import type { MarkStage } from "../PipelineStages.types"; import { LineTarget } from "../targets"; diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index 69e10b9937..009480172a 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -2,7 +2,7 @@ import { NoContainingScopeError } from "../../errors"; import type { Target } from "../../typings/target.types"; import type { ContainingScopeModifier } from "../../typings/targetDescriptor.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; -import getScopeHandler from "../getScopeHandler"; +import getScopeHandler from "./scopeHandlers/getScopeHandler"; import type { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import getLegacyScopeStage from "./getLegacyScopeStage"; @@ -16,16 +16,6 @@ export class ContainingScopeStage implements ModifierStage { constructor(private modifier: ContainingScopeModifier) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - switch (this.modifier.scopeType.type) { - case "token": - case "line": - return this.runNew(target); - default: - return this.runLegacy(context, target); - } - } - - private runNew(target: Target): Target[] { const { isReversed, editor, @@ -33,7 +23,15 @@ export class ContainingScopeStage implements ModifierStage { } = target; const { scopeType } = this.modifier; - const scopeHandler = getScopeHandler(scopeType); + const scopeHandler = getScopeHandler( + scopeType, + target.editor.document.languageId + ); + + if (scopeHandler == null) { + return getLegacyScopeStage(this.modifier).run(context, target); + } + const startScopes = scopeHandler.getScopesTouchingPosition(editor, start); if (startScopes.length === 0) { @@ -59,11 +57,4 @@ export class ContainingScopeStage implements ModifierStage { return [constructScopeRangeTarget(isReversed, startScope, endScope)]; } - - private runLegacy( - context: ProcessedTargetsContext, - target: Target - ): Target[] { - return getLegacyScopeStage(this.modifier).run(context, target); - } } diff --git a/src/processTargets/modifiers/EveryScopeStage.ts b/src/processTargets/modifiers/EveryScopeStage.ts index f5496d0396..d4c8cc9fbc 100644 --- a/src/processTargets/modifiers/EveryScopeStage.ts +++ b/src/processTargets/modifiers/EveryScopeStage.ts @@ -2,7 +2,7 @@ import { NoContainingScopeError } from "../../errors"; import type { Target } from "../../typings/target.types"; import type { EveryScopeModifier } from "../../typings/targetDescriptor.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; -import getScopeHandler from "../getScopeHandler"; +import getScopeHandler from "./scopeHandlers/getScopeHandler"; import type { ModifierStage } from "../PipelineStages.types"; import getLegacyScopeStage from "./getLegacyScopeStage"; import { getPreferredScope, getRightScope } from "./getPreferredScope"; @@ -11,20 +11,17 @@ export class EveryScopeStage implements ModifierStage { constructor(private modifier: EveryScopeModifier) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - switch (this.modifier.scopeType.type) { - case "token": - case "line": - return this.runNew(target); - default: - return this.runLegacy(context, target); - } - } - - private runNew(target: Target): Target[] { const { editor, isReversed, contentRange: range } = target; const { scopeType } = this.modifier; - const scopeHandler = getScopeHandler(scopeType); + const scopeHandler = getScopeHandler( + scopeType, + target.editor.document.languageId + ); + + if (scopeHandler == null) { + return getLegacyScopeStage(this.modifier).run(context, target); + } if (target.hasExplicitRange) { const scopes = scopeHandler.getScopesOverlappingRange(editor, range); @@ -61,11 +58,4 @@ export class EveryScopeStage implements ModifierStage { .getScopes() .map((scope) => scope.getTarget(isReversed)); } - - private runLegacy( - context: ProcessedTargetsContext, - target: Target - ): Target[] { - return getLegacyScopeStage(this.modifier).run(context, target); - } } diff --git a/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/src/processTargets/modifiers/ItemStage/getIterationScope.ts index 4c728cb36f..feba94dcc7 100644 --- a/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ b/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -1,7 +1,7 @@ import { Range, TextEditor } from "vscode"; import { Target } from "../../../typings/target.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; -import { fitRangeToLineContent } from "../scopeHandlers/LineScopeHandler"; +import { fitRangeToLineContent } from "../scopeHandlers"; import { processSurroundingPair } from "../surroundingPair"; import { SurroundingPairInfo } from "../surroundingPair/extractSelectionFromSurroundingPairOffsets"; diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index 4644d53e6b..f9d328b69c 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -3,7 +3,7 @@ import { NoContainingScopeError } from "../../errors"; import { Target } from "../../typings/target.types"; import { RelativeScopeModifier } from "../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../typings/Types"; -import getScopeHandler from "../getScopeHandler"; +import getScopeHandler from "./scopeHandlers/getScopeHandler"; import { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { getPreferredScope } from "./getPreferredScope"; @@ -17,30 +17,26 @@ export class RelativeScopeStage implements ModifierStage { constructor(private modifier: RelativeScopeModifier) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - switch (this.modifier.scopeType.type) { - case "token": - return this.runNew(target); - default: - return runLegacy(this.modifier, context, target); + const scopeHandler = getScopeHandler( + this.modifier.scopeType, + target.editor.document.languageId + ); + + if (scopeHandler == null) { + return runLegacy(this.modifier, context, target); } - } - private runNew(target: Target): Target[] { return this.modifier.offset === 0 - ? this.handleIncludingIntersecting(target) - : this.handleNotIncludingIntersecting(target); + ? this.handleIncludingIntersecting(scopeHandler, target) + : this.handleNotIncludingIntersecting(scopeHandler, target); } - private handleNotIncludingIntersecting(target: Target): Target[] { + private handleNotIncludingIntersecting( + scopeHandler: ScopeHandler, + target: Target + ): Target[] { const { isReversed, editor, contentRange: range } = target; - const { - scopeType, - length: desiredScopeCount, - direction, - offset, - } = this.modifier; - - const scopeHandler = getScopeHandler(scopeType); + const { length: desiredScopeCount, direction, offset } = this.modifier; const index0Scopes = getIndex0Scopes(scopeHandler, editor, range); @@ -100,12 +96,13 @@ export class RelativeScopeStage implements ModifierStage { return [constructScopeRangeTarget(isReversed, proximalScope, distalScope)]; } - private handleIncludingIntersecting(target: Target): Target[] { + private handleIncludingIntersecting( + scopeHandler: ScopeHandler, + target: Target + ): Target[] { const { isReversed, editor, contentRange: range } = target; const { scopeType, length: desiredScopeCount, direction } = this.modifier; - const scopeHandler = getScopeHandler(scopeType); - const index0Scopes = getIndex0Scopes(scopeHandler, editor, range); const index0ScopeCount = index0Scopes.length; diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index 263172dbbf..2c3fc5ef82 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -3,15 +3,23 @@ import { Position, Range, TextEditor } from "vscode"; import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { LineTarget } from "../../targets"; -import { ScopeHandler } from "./scopeHandler.types"; -import { IterationScope, TargetScope } from "./scope.types"; +import type { ScopeHandler } from "./scopeHandler.types"; +import type { IterationScope, TargetScope } from "./scope.types"; import { OutOfRangeError } from "../targetSequenceUtils"; export default class LineScopeHandler implements ScopeHandler { + constructor(_scopeType: ScopeType, _languageId: string) { + // empty + } + get scopeType(): ScopeType { return { type: "line" }; } + get iterationScopeType(): ScopeType { + return { type: "document" }; + } + getScopesTouchingPosition( editor: TextEditor, position: Position diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index 0f91e8b628..b6f584717c 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -1,40 +1,56 @@ -import { TextEditor, Position, Range } from "vscode"; -import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; +import type { Position, Range, TextEditor } from "vscode"; +import type { + Direction, + ScopeType, +} from "../../../typings/targetDescriptor.types"; import { getPreferredScope } from "../getPreferredScope"; -import { ScopeHandler } from "./scopeHandler.types"; -import { IterationScope, TargetScope } from "./scope.types"; import { OutOfRangeError } from "../targetSequenceUtils"; +import { getScopeHandler } from "."; +import type { IterationScope, TargetScope } from "./scope.types"; +import type { ScopeHandler } from "./scopeHandler.types"; export default abstract class NestedScopeHandler implements ScopeHandler { - constructor(private parentScopeHandler: ScopeHandler) {} + private iterationScopeHandler: ScopeHandler; + + constructor( + public readonly iterationScopeType: ScopeType, + languageId: string + ) { + this.iterationScopeHandler = getScopeHandler( + iterationScopeType, + languageId + )!; + } abstract get scopeType(): ScopeType; - protected abstract getScopesInParentScope( - parentScope: TargetScope + protected abstract getScopesInIterationScope( + iterationScope: TargetScope ): TargetScope[]; getScopesTouchingPosition( editor: TextEditor, position: Position ): TargetScope[] { - const parentScope = getPreferredScope( - this.parentScopeHandler.getScopesTouchingPosition(editor, position) + const iterationScope = getPreferredScope( + this.iterationScopeHandler.getScopesTouchingPosition(editor, position) ); - if (parentScope == null) { + if (iterationScope == null) { return []; } - return this.getScopesInParentScope(parentScope).filter(({ domain }) => + return this.getScopesInIterationScope(iterationScope).filter(({ domain }) => domain.contains(position) ); } getScopesOverlappingRange(editor: TextEditor, range: Range): TargetScope[] { - return this.parentScopeHandler + return this.iterationScopeHandler .getScopesOverlappingRange(editor, range) - .flatMap((parentScope) => this.getScopesInParentScope(parentScope)) + .flatMap((iterationScope) => + this.getScopesInIterationScope(iterationScope) + ) .filter(({ domain }) => { const intersection = domain.intersection(range); return intersection != null && !intersection.isEmpty; @@ -45,13 +61,13 @@ export default abstract class NestedScopeHandler implements ScopeHandler { editor: TextEditor, position: Position ): IterationScope[] { - return this.parentScopeHandler + return this.iterationScopeHandler .getScopesTouchingPosition(editor, position) - .map((parentScope) => ({ - domain: parentScope.domain, + .map((iterationScope) => ({ + domain: iterationScope.domain, editor, - isPreferredOver: parentScope.isPreferredOver, - getScopes: () => this.getScopesInParentScope(parentScope), + isPreferredOver: iterationScope.isPreferredOver, + getScopes: () => this.getScopesInIterationScope(iterationScope), })); } @@ -82,14 +98,14 @@ export default abstract class NestedScopeHandler implements ScopeHandler { position: Position, direction: Direction ): Generator { - const containingParentScope = getPreferredScope( - this.parentScopeHandler.getScopesTouchingPosition(editor, position) + const containingIterationScope = getPreferredScope( + this.iterationScopeHandler.getScopesTouchingPosition(editor, position) ); let currentPosition = position; - if (containingParentScope != null) { - yield this.getScopesInParentScope(containingParentScope).filter( + if (containingIterationScope != null) { + yield this.getScopesInIterationScope(containingIterationScope).filter( ({ domain }) => direction === "forward" ? domain.start.isAfterOrEqual(position) @@ -98,24 +114,25 @@ export default abstract class NestedScopeHandler implements ScopeHandler { currentPosition = direction === "forward" - ? containingParentScope.domain.end - : containingParentScope.domain.start; + ? containingIterationScope.domain.end + : containingIterationScope.domain.start; } while (true) { - const parentScope = this.parentScopeHandler.getScopeRelativeToPosition( - editor, - currentPosition, - 1, - direction - ); + const iterationScope = + this.iterationScopeHandler.getScopeRelativeToPosition( + editor, + currentPosition, + 1, + direction + ); - yield this.getScopesInParentScope(parentScope); + yield this.getScopesInIterationScope(iterationScope); currentPosition = direction === "forward" - ? parentScope.domain.end - : parentScope.domain.start; + ? iterationScope.domain.end + : iterationScope.domain.start; } } } diff --git a/src/processTargets/modifiers/scopeHandlers/SimpleRegexScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SimpleRegexScopeHandler.ts deleted file mode 100644 index e2ad25acd0..0000000000 --- a/src/processTargets/modifiers/scopeHandlers/SimpleRegexScopeHandler.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Range, TextEditor } from "vscode"; -import { Target } from "../../../typings/target.types"; -import NestedScopeHandler from "./NestedScopeHandler"; -import { ScopeHandler } from "./scopeHandler.types"; -import { TargetScope } from "./scope.types"; - -export default abstract class SimpleRegexScopeHandler extends NestedScopeHandler { - constructor(parentScopeHandler: ScopeHandler, private regex: RegExp) { - super(parentScopeHandler); - } - - protected abstract constructTarget( - isReversed: boolean, - editor: TextEditor, - contentRange: Range - ): Target; - - protected getScopesInParentScope({ - editor, - domain, - }: TargetScope): TargetScope[] { - return this.getMatchesInRange(editor, domain).map((range) => ({ - editor, - domain: range, - getTarget: (isReversed) => - this.constructTarget(isReversed, editor, range), - })); - } - - private getMatchesInRange(editor: TextEditor, range: Range): Range[] { - const offset = editor.document.offsetAt(range.start); - const text = editor.document.getText(range); - - return [...text.matchAll(this.regex)].map( - (match) => - new Range( - editor.document.positionAt(offset + match.index!), - editor.document.positionAt(offset + match.index! + match[0].length) - ) - ); - } -} diff --git a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts index c41f506bec..2bb8edca5a 100644 --- a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts @@ -7,7 +7,14 @@ import { ScopeHandler } from "./scopeHandler.types"; import { IterationScope, TargetScope } from "./scope.types"; export default class SurroundingPairScopeHandler implements ScopeHandler { - constructor(public readonly scopeType: SurroundingPairScopeType) {} + constructor( + public readonly scopeType: SurroundingPairScopeType, + _languageId: string + ) {} + + get iterationScopeType() { + return undefined; + } getScopesTouchingPosition( _editor: TextEditor, diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index 4b4ca846a1..4991cc0a3d 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -1,22 +1,21 @@ -import { Range, TextEditor } from "vscode"; +import type { Range, TextEditor } from "vscode"; +import { NestedScopeHandler } from "."; import { getMatcher } from "../../../core/tokenizer"; -import { ScopeType } from "../../../typings/targetDescriptor.types"; +import type { ScopeType } from "../../../typings/targetDescriptor.types"; import { getTokensInRange } from "../../../util/getTokensInRange"; import { TokenTarget } from "../../targets"; -import LineScopeHandler from "./LineScopeHandler"; -import NestedScopeHandler from "./NestedScopeHandler"; -import { TargetScope } from "./scope.types"; +import type { TargetScope } from "./scope.types"; export default class TokenScopeHandler extends NestedScopeHandler { - constructor() { - super(new LineScopeHandler()); + constructor(_scopeType: ScopeType, _languageId: string) { + super({ type: "line" }, _languageId); } get scopeType(): ScopeType { return { type: "token" }; } - protected getScopesInParentScope({ + protected getScopesInIterationScope({ editor, domain, }: TargetScope): TargetScope[] { diff --git a/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts new file mode 100644 index 0000000000..1bdcaba211 --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts @@ -0,0 +1,27 @@ +import { LineScopeHandler, TokenScopeHandler } from "."; +import type { ScopeType } from "../../../typings/targetDescriptor.types"; +import type { ScopeHandler } from "./scopeHandler.types"; + +/** + * Returns a scope handler for the given scope type and language id + * + * @param scopeType The scope type for which to get a scope handler + * @param _languageId The language id of the document where the scope handler + * will be used + * @returns A scope handler for the given scope type and language id, or + * undefined if the given scope type / language id combination is still using + * legacy pathways + */ +export default function getScopeHandler( + scopeType: ScopeType, + _languageId: string +): ScopeHandler | undefined { + switch (scopeType.type) { + case "token": + return new TokenScopeHandler(scopeType, _languageId); + case "line": + return new LineScopeHandler(scopeType, _languageId); + default: + return undefined; + } +} diff --git a/src/processTargets/modifiers/scopeHandlers/index.ts b/src/processTargets/modifiers/scopeHandlers/index.ts new file mode 100644 index 0000000000..fc7d25e0cf --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/index.ts @@ -0,0 +1,8 @@ +export * from "./NestedScopeHandler"; +export { default as NestedScopeHandler } from "./NestedScopeHandler"; +export * from "./LineScopeHandler"; +export { default as LineScopeHandler } from "./LineScopeHandler"; +export * from "./TokenScopeHandler"; +export { default as TokenScopeHandler } from "./TokenScopeHandler"; +export * from "./getScopeHandler"; +export { default as getScopeHandler } from "./getScopeHandler"; diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 60476e6d43..f9632358ed 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -33,6 +33,12 @@ export interface ScopeHandler { */ readonly scopeType: ScopeType; + /** + * The scope type of the iteration scope of this scope type, or `undefined` + * if there is no scope type corresponding to the iteration scope. + */ + readonly iterationScopeType: ScopeType | undefined; + /** * Return all scope(s) touching the given position. A scope is considered to * touch a position if its domain contains the position or is directly diff --git a/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts b/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts index 3787c40f24..31c1066b8b 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts @@ -7,7 +7,7 @@ import type { import type { ProcessedTargetsContext } from "../../../typings/Types"; import type { ModifierStage } from "../../PipelineStages.types"; import { ParagraphTarget } from "../../targets"; -import { fitRangeToLineContent } from "../scopeHandlers/LineScopeHandler"; +import { fitRangeToLineContent } from "../scopeHandlers"; export default class implements ModifierStage { constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} diff --git a/src/util/regex.ts b/src/util/regex.ts index 31b535b585..62c06c8f5b 100644 --- a/src/util/regex.ts +++ b/src/util/regex.ts @@ -1,3 +1,5 @@ +import { Range, TextEditor } from "vscode"; + function _rightAnchored(regex: RegExp) { const { source, flags } = regex; @@ -49,3 +51,20 @@ export function matchText(text: string, regex: RegExp): MatchedText[] { text: match[0], })); } + +export function getMatchesInRange( + regex: RegExp, + editor: TextEditor, + range: Range +): Range[] { + const offset = editor.document.offsetAt(range.start); + const text = editor.document.getText(range); + + return [...text.matchAll(regex)].map( + (match) => + new Range( + editor.document.positionAt(offset + match.index!), + editor.document.positionAt(offset + match.index! + match[0].length) + ) + ); +} From 4b52d3c2c335d57747dde6aececb5ba3a3838600 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 17:02:22 +0100 Subject: [PATCH 38/69] Fix regex `lastIndex` issue --- src/core/tokenizer.ts | 16 +++------------- src/util/regex.ts | 4 ++++ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/core/tokenizer.ts b/src/core/tokenizer.ts index 1af43cca2e..58f271dd23 100644 --- a/src/core/tokenizer.ts +++ b/src/core/tokenizer.ts @@ -123,20 +123,10 @@ const tokenMatchersForLanguage: Partial> = ); export function getMatcher(languageId: string): Matcher { - const matcher = + return ( tokenMatchersForLanguage[languageId as SupportedLanguageId] ?? - defaultMatcher; - - return { - ...matcher, - // TODO Why is this necessary??????????? - identifierMatcher: new RegExp(matcher.identifierMatcher), - }; - - // return ( - // tokenMatchersForLanguage[languageId as SupportedLanguageId] ?? - // defaultMatcher - // ); + defaultMatcher + ); } export function tokenize( diff --git a/src/util/regex.ts b/src/util/regex.ts index 62c06c8f5b..dd33e14b31 100644 --- a/src/util/regex.ts +++ b/src/util/regex.ts @@ -37,6 +37,10 @@ export function matchAll( regex: RegExp, mapfn: (v: RegExpMatchArray, k: number) => T ) { + // Reset the regex to start at the beginning of string, in case the regex has + // been used before. + // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#finding_successive_matches + regex.lastIndex = 0; return Array.from(text.matchAll(regex), mapfn); } From e9b30cc4f200426cdeeb481eed89e18373c91d4e Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 17:02:26 +0100 Subject: [PATCH 39/69] More cleanup --- .../scopeHandlers/LineScopeHandler.ts | 11 ++----- .../scopeHandlers/NestedScopeHandler.ts | 30 +++++++++++-------- .../SurroundingPairScopeHandler.ts | 8 ++--- .../scopeHandlers/TokenScopeHandler.ts | 9 +++--- 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index 2c3fc5ef82..a312865c33 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -8,18 +8,13 @@ import type { IterationScope, TargetScope } from "./scope.types"; import { OutOfRangeError } from "../targetSequenceUtils"; export default class LineScopeHandler implements ScopeHandler { + public readonly scopeType: ScopeType = { type: "line" }; + public readonly iterationScopeType: ScopeType = { type: "document" }; + constructor(_scopeType: ScopeType, _languageId: string) { // empty } - get scopeType(): ScopeType { - return { type: "line" }; - } - - get iterationScopeType(): ScopeType { - return { type: "document" }; - } - getScopesTouchingPosition( editor: TextEditor, position: Position diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index b6f584717c..05ae07c4bb 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -10,24 +10,28 @@ import type { IterationScope, TargetScope } from "./scope.types"; import type { ScopeHandler } from "./scopeHandler.types"; export default abstract class NestedScopeHandler implements ScopeHandler { - private iterationScopeHandler: ScopeHandler; - - constructor( - public readonly iterationScopeType: ScopeType, - languageId: string - ) { - this.iterationScopeHandler = getScopeHandler( - iterationScopeType, - languageId - )!; - } - - abstract get scopeType(): ScopeType; + public abstract readonly scopeType: ScopeType; + public abstract readonly iterationScopeType: ScopeType; protected abstract getScopesInIterationScope( iterationScope: TargetScope ): TargetScope[]; + private _iterationScopeHandler: ScopeHandler | undefined; + + constructor(private languageId: string) {} + + private get iterationScopeHandler(): ScopeHandler { + if (this._iterationScopeHandler == null) { + this._iterationScopeHandler = getScopeHandler( + this.iterationScopeType, + this.languageId + )!; + } + + return this._iterationScopeHandler; + } + getScopesTouchingPosition( editor: TextEditor, position: Position diff --git a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts index 2bb8edca5a..0a1c373cc1 100644 --- a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts @@ -7,21 +7,19 @@ import { ScopeHandler } from "./scopeHandler.types"; import { IterationScope, TargetScope } from "./scope.types"; export default class SurroundingPairScopeHandler implements ScopeHandler { + public readonly iterationScopeType = undefined; + constructor( public readonly scopeType: SurroundingPairScopeType, _languageId: string ) {} - get iterationScopeType() { - return undefined; - } - getScopesTouchingPosition( _editor: TextEditor, _position: Position ): TargetScope[] { // TODO: Run existing surrounding pair code on empty range constructed from - // position, returning both if position is adjacent to to + // position, returning both if position is adjacent to two throw new Error("Method not implemented."); } diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index 4991cc0a3d..093729431a 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -7,12 +7,11 @@ import { TokenTarget } from "../../targets"; import type { TargetScope } from "./scope.types"; export default class TokenScopeHandler extends NestedScopeHandler { - constructor(_scopeType: ScopeType, _languageId: string) { - super({ type: "line" }, _languageId); - } + public readonly scopeType: ScopeType = { type: "token" }; + public readonly iterationScopeType: ScopeType = { type: "line" }; - get scopeType(): ScopeType { - return { type: "token" }; + constructor(_scopeType: ScopeType, _languageId: string) { + super(_languageId); } protected getScopesInIterationScope({ From 4e7920caab24b43671eebcdf7d36c9282c52ae77 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 17:06:00 +0100 Subject: [PATCH 40/69] More cleanup --- .../modifiers/RelativeScopeStage.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index f9d328b69c..c9a1c39c8c 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -1,17 +1,17 @@ -import { Position, Range, TextEditor } from "vscode"; +import type { Position, Range, TextEditor } from "vscode"; import { NoContainingScopeError } from "../../errors"; -import { Target } from "../../typings/target.types"; -import { RelativeScopeModifier } from "../../typings/targetDescriptor.types"; -import { ProcessedTargetsContext } from "../../typings/Types"; -import getScopeHandler from "./scopeHandlers/getScopeHandler"; -import { ModifierStage } from "../PipelineStages.types"; +import type { Target } from "../../typings/target.types"; +import type { RelativeScopeModifier } from "../../typings/targetDescriptor.types"; +import type { ProcessedTargetsContext } from "../../typings/Types"; +import { strictlyContains } from "../../util/rangeUtils"; +import type { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { getPreferredScope } from "./getPreferredScope"; import { runLegacy } from "./relativeScopeLegacy"; -import { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; -import { TargetScope } from "./scopeHandlers/scope.types"; +import getScopeHandler from "./scopeHandlers/getScopeHandler"; +import type { TargetScope } from "./scopeHandlers/scope.types"; +import type { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; import { TooFewScopesError } from "./TooFewScopesError"; -import { strictlyContains } from "../../util/rangeUtils"; export class RelativeScopeStage implements ModifierStage { constructor(private modifier: RelativeScopeModifier) {} From 527ad1860d8cbd143799e845f627b56d477d40c0 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:18:48 +0100 Subject: [PATCH 41/69] More cleanup --- .../modifiers/scopeHandlers/LineScopeHandler.ts | 4 ++-- .../scopeHandlers/SurroundingPairScopeHandler.ts | 4 ++-- .../modifiers/scopeHandlers/scope.types.ts | 4 ++-- .../modifiers/scopeHandlers/scopeHandler.types.ts | 9 ++++++--- src/typings/target.types.ts | 6 +++--- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index a312865c33..8977f00fda 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -3,9 +3,9 @@ import { Position, Range, TextEditor } from "vscode"; import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { LineTarget } from "../../targets"; -import type { ScopeHandler } from "./scopeHandler.types"; -import type { IterationScope, TargetScope } from "./scope.types"; import { OutOfRangeError } from "../targetSequenceUtils"; +import type { IterationScope, TargetScope } from "./scope.types"; +import type { ScopeHandler } from "./scopeHandler.types"; export default class LineScopeHandler implements ScopeHandler { public readonly scopeType: ScopeType = { type: "line" }; diff --git a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts index 0a1c373cc1..e9edc7aa7a 100644 --- a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts @@ -1,10 +1,10 @@ -import { TextEditor, Position, Range } from "vscode"; +import { Position, Range, TextEditor } from "vscode"; import { Direction, SurroundingPairScopeType, } from "../../../typings/targetDescriptor.types"; -import { ScopeHandler } from "./scopeHandler.types"; import { IterationScope, TargetScope } from "./scope.types"; +import { ScopeHandler } from "./scopeHandler.types"; export default class SurroundingPairScopeHandler implements ScopeHandler { public readonly iterationScopeType = undefined; diff --git a/src/processTargets/modifiers/scopeHandlers/scope.types.ts b/src/processTargets/modifiers/scopeHandlers/scope.types.ts index e7251887b3..99b79bcfa8 100644 --- a/src/processTargets/modifiers/scopeHandlers/scope.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scope.types.ts @@ -1,5 +1,5 @@ -import { Range, TextEditor } from "vscode"; -import { Target } from "../../../typings/target.types"; +import type { Range, TextEditor } from "vscode"; +import type { Target } from "../../../typings/target.types"; /** * A range in the document within which a particular scope type is considered diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index f9632358ed..eef54f356c 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -1,6 +1,9 @@ -import { Position, Range, TextEditor } from "vscode"; -import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; -import { TargetScope, IterationScope } from "./scope.types"; +import type { Position, Range, TextEditor } from "vscode"; +import type { + Direction, + ScopeType, +} from "../../../typings/targetDescriptor.types"; +import type { TargetScope, IterationScope } from "./scope.types"; /** * Represents a scope type. The functions in this interface allow us to find diff --git a/src/typings/target.types.ts b/src/typings/target.types.ts index e22eb0498c..02cb70c70e 100644 --- a/src/typings/target.types.ts +++ b/src/typings/target.types.ts @@ -1,4 +1,4 @@ -import { Range, Selection, TextEditor } from "vscode"; +import type { Range, Selection, TextEditor } from "vscode"; // NB: We import `Target` below just so that @link below resolves. Once one of // the following issues are fixed, we can either remove the above line or // switch to `{import("foo")}` syntax in the `{@link}` tag. @@ -16,8 +16,8 @@ import type { // eslint-disable-next-line @typescript-eslint/no-unused-vars UntypedTarget, } from "../processTargets/targets"; -import { Position } from "./targetDescriptor.types"; -import { EditWithRangeUpdater } from "./Types"; +import type { Position } from "./targetDescriptor.types"; +import type { EditWithRangeUpdater } from "./Types"; export interface EditNewCommandContext { type: "command"; From d73fae6d2d488775f123547d71e25b2bb9b7001b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:18:52 +0100 Subject: [PATCH 42/69] Add `ancestorIndex` in prepartion for #124 --- .../scopeHandlers/LineScopeHandler.ts | 8 +++++- .../scopeHandlers/NestedScopeHandler.ts | 8 +++++- .../SurroundingPairScopeHandler.ts | 4 ++- .../scopeHandlers/scopeHandler.types.ts | 28 +++++++++++++++---- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index 8977f00fda..e589de3be9 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -1,5 +1,6 @@ import { range } from "lodash"; import { Position, Range, TextEditor } from "vscode"; +import { NoContainingScopeError } from "../../../errors"; import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { LineTarget } from "../../targets"; @@ -17,8 +18,13 @@ export default class LineScopeHandler implements ScopeHandler { getScopesTouchingPosition( editor: TextEditor, - position: Position + position: Position, + ancestorIndex: number = 0 ): TargetScope[] { + if (ancestorIndex !== 0) { + throw new NoContainingScopeError(this.scopeType.type); + } + return [lineNumberToScope(editor, position.line)]; } diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index 05ae07c4bb..67cadb45e9 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -8,6 +8,7 @@ import { OutOfRangeError } from "../targetSequenceUtils"; import { getScopeHandler } from "."; import type { IterationScope, TargetScope } from "./scope.types"; import type { ScopeHandler } from "./scopeHandler.types"; +import { NoContainingScopeError } from "../../../errors"; export default abstract class NestedScopeHandler implements ScopeHandler { public abstract readonly scopeType: ScopeType; @@ -34,8 +35,13 @@ export default abstract class NestedScopeHandler implements ScopeHandler { getScopesTouchingPosition( editor: TextEditor, - position: Position + position: Position, + ancestorIndex: number = 0 ): TargetScope[] { + if (ancestorIndex !== 0) { + throw new NoContainingScopeError(this.scopeType.type); + } + const iterationScope = getPreferredScope( this.iterationScopeHandler.getScopesTouchingPosition(editor, position) ); diff --git a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts index e9edc7aa7a..b35256049d 100644 --- a/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler.ts @@ -16,10 +16,12 @@ export default class SurroundingPairScopeHandler implements ScopeHandler { getScopesTouchingPosition( _editor: TextEditor, - _position: Position + _position: Position, + _ancestorIndex: number = 0 ): TargetScope[] { // TODO: Run existing surrounding pair code on empty range constructed from // position, returning both if position is adjacent to two + // TODO: Handle ancestor index throw new Error("Method not implemented."); } diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index eef54f356c..ccaf6d15ed 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -54,18 +54,34 @@ export interface ScopeHandler { * * If the position is directly adjacent to two scopes, return both. You can * use {@link TargetScope.isPreferredOver} to indicate which one should have - * precedence. If no scope contains the given position, return an empty - * list. + * precedence. If no scope contains the given position, return an empty list. + * + * Note that if this scope type is hierarchical, return only minimal scopes if + * {@link ancestorIndex} is omitted or is 0. Ie if scope A and scope B both + * touch {@link position}, and scope A contains scope B, return scope B but + * not scope A. + * + * If {@link ancestorIndex} is supplied and is greater than 0, throw a + * {@link NoContainingScopeError} if the scope type is not hierarchical. + * + * If the scope type is hierarchical, then if {@link ancestorIndex} is 1, + * return all scopes touching {@link position} that have a child that is a + * minimal scope touching {@link position}. If {@link ancestorIndex} is 2, + * return all scopes touching {@link position} that have a child with + * {@link ancestorIndex} of 1 with respect to {@link position}, etc. + * + * The {@link ancestorIndex} parameter is primarily to be used by `"grand"` + * scopes (#124). * - * Note that if this scope type is hierarchical, return only minimal scopes, - * ie if scope A and scope B both touch {@link position}, and scope A contains - * scope B, return scope B but not scope A. * @param editor The editor containing {@link position} * @param position The position from which to expand + * @param ancestorIndex If supplied, skip this many ancestors up the + * hierarchy. */ getScopesTouchingPosition( editor: TextEditor, - position: Position + position: Position, + ancestorIndex?: number ): TargetScope[]; /** From 0b082cb04fdb83004046e7ab7d1e8b89f57bdd94 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:00:39 +0100 Subject: [PATCH 43/69] Add more jsdocs --- .../modifiers/ContainingScopeStage.ts | 23 +++++++ .../modifiers/EveryScopeStage.ts | 66 +++++++++++++++---- .../modifiers/getPreferredScope.ts | 20 ++++++ .../scopeHandlers/getScopeHandler.ts | 9 ++- 4 files changed, 104 insertions(+), 14 deletions(-) diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index 009480172a..a2b5f4d92e 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -12,6 +12,20 @@ import { getRightScope, } from "./getPreferredScope"; +/** + * This modifier stage expands from the input target to the smallest containing + * scope. We proceed as follows: + * + * 1. Expand to smallest scope(s) touching start position of input target's + * content range + * 2. If input target has an empty content range, return the start scope, + * breaking ties as defined by {@link getPreferredScope} when more than one + * scope touches content range start + * 3. If end of input target is weakly contained by the domain of the rightmost + * start scope, return rightmost start scope. + * 4. Expand from end of input target and form a range from rightmost start + * scope through leftmost end scope. + */ export class ContainingScopeStage implements ModifierStage { constructor(private modifier: ContainingScopeModifier) {} @@ -39,15 +53,24 @@ export class ContainingScopeStage implements ModifierStage { } if (end.isEqual(start)) { + // Input target is empty; return the preferred scope touching target return [getPreferredScope(startScopes)!.getTarget(isReversed)]; } + // Target is non-empty; use the rightmost scope touching `startScope` + // because that will have non-empty overlap with input content range const startScope = getRightScope(startScopes)!; if (startScope.domain.contains(end)) { + // End of input target is contained in domain of start scope; return start + // scope return [startScope.getTarget(isReversed)]; } + // End of input target is after end of start scope; we need to make a range + // between start and end scopes. For the end scope, we break ties to the + // left so that the scope will have non-empty overlap with input target + // content range. const endScopes = scopeHandler.getScopesTouchingPosition(editor, end); const endScope = getLeftScope(endScopes); diff --git a/src/processTargets/modifiers/EveryScopeStage.ts b/src/processTargets/modifiers/EveryScopeStage.ts index d4c8cc9fbc..8e05c441da 100644 --- a/src/processTargets/modifiers/EveryScopeStage.ts +++ b/src/processTargets/modifiers/EveryScopeStage.ts @@ -2,20 +2,37 @@ import { NoContainingScopeError } from "../../errors"; import type { Target } from "../../typings/target.types"; import type { EveryScopeModifier } from "../../typings/targetDescriptor.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; -import getScopeHandler from "./scopeHandlers/getScopeHandler"; import type { ModifierStage } from "../PipelineStages.types"; import getLegacyScopeStage from "./getLegacyScopeStage"; import { getPreferredScope, getRightScope } from "./getPreferredScope"; +import getScopeHandler from "./scopeHandlers/getScopeHandler"; +import { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; +/** + * This modifier returns all scopes intersecting the input target if the target + * has an explicit range (ie {@link Target.hasExplicitRange} is `true`). If the + * target does not have an explicit range, this modifier returns all scopes in + * the canonical iteration scope defined by the scope handler. + * + * We proceed as follows: + * + * 1. If target has an explicit range, just return all targets returned from + * {@link ScopeHandler.getScopesOverlappingRange}. + * 2. Otherwise, get the iteration scope for the start of the input target. + * 3. If two iteration scopes touch the start position, choose the preferred one + * if input target has empty content range, otherwise prefer the rightmost + * one, as that will have an overlap with the target input content range. + * 3. If the domain of the iteration scope doesn't contain the end of the input + * target, we error, because this situation shouldn't really happen, as + * targets without explicit range tend to be small. + * 4. Return all targets in the iteration scope + */ export class EveryScopeStage implements ModifierStage { constructor(private modifier: EveryScopeModifier) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - const { editor, isReversed, contentRange: range } = target; - const { scopeType } = this.modifier; - const scopeHandler = getScopeHandler( - scopeType, + this.modifier.scopeType, target.editor.document.languageId ); @@ -23,27 +40,50 @@ export class EveryScopeStage implements ModifierStage { return getLegacyScopeStage(this.modifier).run(context, target); } - if (target.hasExplicitRange) { - const scopes = scopeHandler.getScopesOverlappingRange(editor, range); + return target.hasExplicitRange + ? this.handleExplicitRangeTarget(scopeHandler, target) + : this.handleNoExplicitRangeTarget(scopeHandler, target); + } + + private handleExplicitRangeTarget( + scopeHandler: ScopeHandler, + target: Target + ): Target[] { + const { editor, isReversed, contentRange: range } = target; + const { scopeType } = this.modifier; - if (scopes.length === 0) { - throw new NoContainingScopeError(scopeType.type); - } + const scopes = scopeHandler.getScopesOverlappingRange(editor, range); - return scopes.map((scope) => scope.getTarget(isReversed)); + if (scopes.length === 0) { + throw new NoContainingScopeError(scopeType.type); } - const { start, end } = range; + return scopes.map((scope) => scope.getTarget(isReversed)); + } + + private handleNoExplicitRangeTarget( + scopeHandler: ScopeHandler, + target: Target + ): Target[] { + const { + editor, + isReversed, + contentRange: { start, end }, + } = target; + const { scopeType } = this.modifier; const startIterationScopes = scopeHandler.getIterationScopesTouchingPosition(editor, start); + // If target is empty, use the preferred scope; otherwise use the rightmost + // scope, as that one will have non-empty intersection with input target + // content range const startIterationScope = end.isEqual(start) ? getPreferredScope(startIterationScopes) : getRightScope(startIterationScopes); if (startIterationScope == null) { - throw new NoContainingScopeError(this.modifier.scopeType.type); + throw new NoContainingScopeError(scopeType.type); } if (!startIterationScope.domain.contains(end)) { diff --git a/src/processTargets/modifiers/getPreferredScope.ts b/src/processTargets/modifiers/getPreferredScope.ts index 7960204678..04475f8b30 100644 --- a/src/processTargets/modifiers/getPreferredScope.ts +++ b/src/processTargets/modifiers/getPreferredScope.ts @@ -1,5 +1,13 @@ import { Scope } from "./scopeHandlers/scope.types"; +/** + * Given a list of scopes, returns the preferred scope, or `undefined` if + * {@link scopes} is empty. If {@link Scope.isPreferredOver} is defined and + * returns a boolean, we use that preference. Otherwise we just prefer the + * rightmost scope. + * @param scopes A list of scopes to choose from + * @returns A single preferred scope, or `undefined` if {@link scopes} is empty + */ export function getPreferredScope(scopes: T[]): T | undefined { return getScopeHelper( scopes, @@ -9,12 +17,24 @@ export function getPreferredScope(scopes: T[]): T | undefined { ); } +/** + * Given a list of scopes, returns the leftmost scope, or `undefined` if + * {@link scopes} is empty. + * @param scopes A list of scopes to choose from + * @returns A single preferred scope, or `undefined` if {@link scopes} is empty + */ export function getLeftScope(scopes: T[]): T | undefined { return getScopeHelper(scopes, (scope1, scope2) => scope1.domain.start.isBefore(scope2.domain.start) ); } +/** + * Given a list of scopes, returns the rightmost scope, or `undefined` if + * {@link scopes} is empty. + * @param scopes A list of scopes to choose from + * @returns A single preferred scope, or `undefined` if {@link scopes} is empty + */ export function getRightScope(scopes: T[]): T | undefined { return getScopeHelper(scopes, (scope1, scope2) => scope1.domain.start.isAfter(scope2.domain.start) diff --git a/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts index 1bdcaba211..3a19564fae 100644 --- a/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts @@ -3,7 +3,14 @@ import type { ScopeType } from "../../../typings/targetDescriptor.types"; import type { ScopeHandler } from "./scopeHandler.types"; /** - * Returns a scope handler for the given scope type and language id + * Returns a scope handler for the given scope type and language id, or + * undefined if the given scope type / language id combination is still using + * legacy pathways. + * + * Note that once all our scope types are migrated to the new scope handler + * setup for all languages, we can stop returning `undefined`, change the return + * type of this function, and remove the legacy checks in the clients of this + * function. * * @param scopeType The scope type for which to get a scope handler * @param _languageId The language id of the document where the scope handler From ed93d3fa535120d63f69602faa5c7f58ce5f9c2a Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:04:59 +0100 Subject: [PATCH 44/69] More docs --- .../modifiers/ContainingScopeStage.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index a2b5f4d92e..ec21df7c48 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -20,11 +20,15 @@ import { * content range * 2. If input target has an empty content range, return the start scope, * breaking ties as defined by {@link getPreferredScope} when more than one - * scope touches content range start - * 3. If end of input target is weakly contained by the domain of the rightmost - * start scope, return rightmost start scope. - * 4. Expand from end of input target and form a range from rightmost start - * scope through leftmost end scope. + * scope touches content range + * 3. Otherwise, if end of input target is weakly contained by the domain of the + * rightmost start scope, return rightmost start scope. We return rightmost + * because that will have non-empty intersection with input target content + * range. + * 4. Otherwise, expand from end of input target and form a range from rightmost + * start scope through leftmost end scope. We use rightmost start scope and + * leftmost end scope because those will have non-empty intersection with + * input target content range. */ export class ContainingScopeStage implements ModifierStage { constructor(private modifier: ContainingScopeModifier) {} From b1bb34c87375de1e96093b4a4cf42aa05263e6ca Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:07:47 +0100 Subject: [PATCH 45/69] More docs --- src/processTargets/modifiers/EveryScopeStage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/processTargets/modifiers/EveryScopeStage.ts b/src/processTargets/modifiers/EveryScopeStage.ts index 8e05c441da..d6d995847d 100644 --- a/src/processTargets/modifiers/EveryScopeStage.ts +++ b/src/processTargets/modifiers/EveryScopeStage.ts @@ -12,7 +12,8 @@ import { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; * This modifier returns all scopes intersecting the input target if the target * has an explicit range (ie {@link Target.hasExplicitRange} is `true`). If the * target does not have an explicit range, this modifier returns all scopes in - * the canonical iteration scope defined by the scope handler. + * the canonical iteration scope defined by the scope handler in + * {@link ScopeHandler.getIterationScopesTouchingPosition}. * * We proceed as follows: * From 616e8282412d7bc6b10815e79a455f7b2008dfb4 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:24:53 +0100 Subject: [PATCH 46/69] docstrings --- .../modifiers/constructScopeRangeTarget.ts | 12 ++++++++++++ src/processTargets/modifiers/getLegacyScopeStage.ts | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/processTargets/modifiers/constructScopeRangeTarget.ts b/src/processTargets/modifiers/constructScopeRangeTarget.ts index 84a97bff35..f73e731d08 100644 --- a/src/processTargets/modifiers/constructScopeRangeTarget.ts +++ b/src/processTargets/modifiers/constructScopeRangeTarget.ts @@ -1,6 +1,18 @@ import { Target } from "../../typings/target.types"; import { TargetScope } from "./scopeHandlers/scope.types"; +/** + * Constructs a target consisting of a range between {@link scope1} and + * {@link scope2}. The order of {@link scope1} and {@link scope2} doesn't matter; + * this function will automatically figure out which one should be the start of + * the range, and which should be the end. + * @param isReversed Whether the returned target should have active before + * anchor + * @param scope1 The scope forming one end of the range + * @param scope2 A scope forming another end of the range + * @returns A target consisting of a range between {@link scope1} and + * {@link scope2} + */ export function constructScopeRangeTarget( isReversed: boolean, scope1: TargetScope, diff --git a/src/processTargets/modifiers/getLegacyScopeStage.ts b/src/processTargets/modifiers/getLegacyScopeStage.ts index ed34b7ecd6..f3b32e9d91 100644 --- a/src/processTargets/modifiers/getLegacyScopeStage.ts +++ b/src/processTargets/modifiers/getLegacyScopeStage.ts @@ -22,6 +22,19 @@ import { import { CharacterStage, WordStage } from "./scopeTypeStages/SubTokenStages"; import SurroundingPairStage from "./SurroundingPairStage"; +/** + * Any scope type that has not been fully migrated to the new + * {@link ScopeHandler} setup should have a branch in this `switch` statement. + * Once the scope type is fully migrated, remove the branch and the legacy + * modifier stage. + * + * Note that it is possible for a scope type to be partially migrated. For + * example, we could support modern scope handlers for a certain scope type in + * Ruby, but not yet in Python. + * + * @param modifier The modifier for which to get the modifier stage + * @returns A scope stage implementing the modifier for the given scope type + */ export default function getLegacyScopeStage( modifier: ContainingScopeModifier | EveryScopeModifier ): ModifierStage { From eaa33180192a3c5aac054042cded81a5505b56c4 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:29:51 +0100 Subject: [PATCH 47/69] Improve hierarchical error type --- .../scopeHandlers/LineScopeHandler.ts | 4 ++-- .../scopeHandlers/NestedScopeHandler.ts | 6 +++--- .../NotHierarchicalScopeError.ts | 19 +++++++++++++++++++ .../scopeHandlers/scopeHandler.types.ts | 2 +- .../modifiers/scopeHandlers/scopeTypeUtil.ts | 5 +++++ 5 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 src/processTargets/modifiers/scopeHandlers/NotHierarchicalScopeError.ts create mode 100644 src/processTargets/modifiers/scopeHandlers/scopeTypeUtil.ts diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index e589de3be9..d7e4b1d23d 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -1,10 +1,10 @@ import { range } from "lodash"; import { Position, Range, TextEditor } from "vscode"; -import { NoContainingScopeError } from "../../../errors"; import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { LineTarget } from "../../targets"; import { OutOfRangeError } from "../targetSequenceUtils"; +import NotHierarchicalScopeError from "./NotHierarchicalScopeError"; import type { IterationScope, TargetScope } from "./scope.types"; import type { ScopeHandler } from "./scopeHandler.types"; @@ -22,7 +22,7 @@ export default class LineScopeHandler implements ScopeHandler { ancestorIndex: number = 0 ): TargetScope[] { if (ancestorIndex !== 0) { - throw new NoContainingScopeError(this.scopeType.type); + throw new NotHierarchicalScopeError(this.scopeType); } return [lineNumberToScope(editor, position.line)]; diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index 67cadb45e9..2ddec75239 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -1,14 +1,14 @@ import type { Position, Range, TextEditor } from "vscode"; +import { getScopeHandler } from "."; import type { Direction, ScopeType, } from "../../../typings/targetDescriptor.types"; import { getPreferredScope } from "../getPreferredScope"; import { OutOfRangeError } from "../targetSequenceUtils"; -import { getScopeHandler } from "."; +import NotHierarchicalScopeError from "./NotHierarchicalScopeError"; import type { IterationScope, TargetScope } from "./scope.types"; import type { ScopeHandler } from "./scopeHandler.types"; -import { NoContainingScopeError } from "../../../errors"; export default abstract class NestedScopeHandler implements ScopeHandler { public abstract readonly scopeType: ScopeType; @@ -39,7 +39,7 @@ export default abstract class NestedScopeHandler implements ScopeHandler { ancestorIndex: number = 0 ): TargetScope[] { if (ancestorIndex !== 0) { - throw new NoContainingScopeError(this.scopeType.type); + throw new NotHierarchicalScopeError(this.scopeType); } const iterationScope = getPreferredScope( diff --git a/src/processTargets/modifiers/scopeHandlers/NotHierarchicalScopeError.ts b/src/processTargets/modifiers/scopeHandlers/NotHierarchicalScopeError.ts new file mode 100644 index 0000000000..c8464c3dc7 --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/NotHierarchicalScopeError.ts @@ -0,0 +1,19 @@ +import { ScopeType } from "../../../typings/targetDescriptor.types"; +import { scopeTypeToString } from "./scopeTypeUtil"; + +/** + * Throw this error when the user requests a hierarchical feature of a scope + * that is not hierarchical, eg `"grand line"`. + */ +export default class NotHierarchicalScopeError extends Error { + /** + * + * @param scopeType The scopeType for the failed match to show to the user + */ + constructor(scopeType: ScopeType) { + super( + `Cannot use hierarchical modifiers on ${scopeTypeToString(scopeType)}.` + ); + this.name = "NotHierarchicalScopeError"; + } +} diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index ccaf6d15ed..f1a74b9ad7 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -62,7 +62,7 @@ export interface ScopeHandler { * not scope A. * * If {@link ancestorIndex} is supplied and is greater than 0, throw a - * {@link NoContainingScopeError} if the scope type is not hierarchical. + * {@link NotHierarchicalScopeError} if the scope type is not hierarchical. * * If the scope type is hierarchical, then if {@link ancestorIndex} is 1, * return all scopes touching {@link position} that have a child that is a diff --git a/src/processTargets/modifiers/scopeHandlers/scopeTypeUtil.ts b/src/processTargets/modifiers/scopeHandlers/scopeTypeUtil.ts new file mode 100644 index 0000000000..cc886fcb5e --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/scopeTypeUtil.ts @@ -0,0 +1,5 @@ +import { ScopeType } from "../../../typings/targetDescriptor.types"; + +export function scopeTypeToString(scopeType: ScopeType) { + return scopeType.type; +} From 62a2e6e0a7c24015cebeca9e01f498b782699c6b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:32:08 +0100 Subject: [PATCH 48/69] Docs --- src/processTargets/modifiers/constructScopeRangeTarget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processTargets/modifiers/constructScopeRangeTarget.ts b/src/processTargets/modifiers/constructScopeRangeTarget.ts index f73e731d08..92fd2a9388 100644 --- a/src/processTargets/modifiers/constructScopeRangeTarget.ts +++ b/src/processTargets/modifiers/constructScopeRangeTarget.ts @@ -8,7 +8,7 @@ import { TargetScope } from "./scopeHandlers/scope.types"; * the range, and which should be the end. * @param isReversed Whether the returned target should have active before * anchor - * @param scope1 The scope forming one end of the range + * @param scope1 A scope forming one end of the range * @param scope2 A scope forming another end of the range * @returns A target consisting of a range between {@link scope1} and * {@link scope2} From f83493c5b5fe42b256b8376c9a8b347418346abb Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:48:08 +0100 Subject: [PATCH 49/69] More minor dog tweaks --- src/processTargets/modifiers/scopeHandlers/scope.types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/scope.types.ts b/src/processTargets/modifiers/scopeHandlers/scope.types.ts index 99b79bcfa8..e8ca19f470 100644 --- a/src/processTargets/modifiers/scopeHandlers/scope.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scope.types.ts @@ -2,10 +2,10 @@ import type { Range, TextEditor } from "vscode"; import type { Target } from "../../../typings/target.types"; /** - * A range in the document within which a particular scope type is considered - * the canonical instance of the given region. We use this type both to define - * the domain within which a target is canonical, and the domain within which - * an iteration scope is canonical. + * A range in the document within which a particular scope is considered the + * canonical instance of the given scope type. We use this type both to define + * the domain within which a target is canonical, and the domain within which an + * iteration scope is canonical. */ export interface Scope { /** From af9c0f7460b2989a512bc2bd0f4a24f74f0c3040 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:54:34 +0100 Subject: [PATCH 50/69] More docs --- .../scopeHandlers/scopeHandler.types.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index f1a74b9ad7..172e339c48 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -37,8 +37,14 @@ export interface ScopeHandler { readonly scopeType: ScopeType; /** - * The scope type of the iteration scope of this scope type, or `undefined` - * if there is no scope type corresponding to the iteration scope. + * The scope type of the iteration scope of this scope type, or `undefined` if + * there is no scope type corresponding to the iteration scope. Note that + * even when this property is `undefined`, all scope types should have an + * iteration scope; it just may not correspond to one of our first-class scope + * types. + * + * FIXME: Revisit this; maybe we should always find a way to make the + * iteration scope a scope type. */ readonly iterationScopeType: ScopeType | undefined; @@ -66,9 +72,11 @@ export interface ScopeHandler { * * If the scope type is hierarchical, then if {@link ancestorIndex} is 1, * return all scopes touching {@link position} that have a child that is a - * minimal scope touching {@link position}. If {@link ancestorIndex} is 2, - * return all scopes touching {@link position} that have a child with - * {@link ancestorIndex} of 1 with respect to {@link position}, etc. + * minimal scope touching {@link position} (ie they have a child that has an + * {@link ancestorIndex} of 1 with respect to {@link position}). If + * {@link ancestorIndex} is 2, return all scopes touching {@link position} + * that have a child with {@link ancestorIndex} of 1 with respect to + * {@link position}, etc. * * The {@link ancestorIndex} parameter is primarily to be used by `"grand"` * scopes (#124). From 65f8a13ed8d0dcbf9b4a9c33f225fed1bc95395e Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 20:03:31 +0100 Subject: [PATCH 51/69] More docs --- src/util/rangeUtils.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/util/rangeUtils.ts b/src/util/rangeUtils.ts index 37ac0cbc36..c309199762 100644 --- a/src/util/rangeUtils.ts +++ b/src/util/rangeUtils.ts @@ -36,6 +36,17 @@ export function getRangeLength(editor: TextEditor, range: Range) { editor.document.offsetAt(range.start); } +/** + * Returns + * + * ``` + * range1.start < range2.start && range1.end > range2.end + * ``` + * @param range1 One of the ranges to compare + * @param range2 The other range to compare + * @returns A boolean indicating whether {@link range1} completely contains + * {@link range2} without it touching either boundary + */ export function strictlyContains(range1: Range, range2: Range): boolean { return range1.start.isBefore(range2.start) && range1.end.isAfter(range2.end); } From ab7feae175fe20aec5e36c346572eacc28510d3f Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 19 Oct 2022 20:28:40 +0100 Subject: [PATCH 52/69] Doc strings and a couple tests --- .../scopeHandlers/NestedScopeHandler.ts | 72 +++++++++++++++---- .../recorded/containingScope/clearWord.yml | 23 ++++++ .../recorded/containingScope/clearWord2.yml | 23 ++++++ 3 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearWord.yml create mode 100644 src/test/suite/fixtures/recorded/containingScope/clearWord2.yml diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index 2ddec75239..30c85d11ca 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -4,16 +4,32 @@ import type { Direction, ScopeType, } from "../../../typings/targetDescriptor.types"; -import { getPreferredScope } from "../getPreferredScope"; +import { getLeftScope, getRightScope } from "../getPreferredScope"; import { OutOfRangeError } from "../targetSequenceUtils"; import NotHierarchicalScopeError from "./NotHierarchicalScopeError"; import type { IterationScope, TargetScope } from "./scope.types"; import type { ScopeHandler } from "./scopeHandler.types"; +/** + * This class can be used to define scope types that are most easily defined by + * simply getting all scopes in an instance of a parent scope type and then + * operating on those. A good example is regex-based scope types where the + * regex can't cross line boundaries. In this case the + * {@link iterationScopeType} will be `line`, and we just return a list of all + * regex matches to this base class and let it handle the rest. + */ export default abstract class NestedScopeHandler implements ScopeHandler { public abstract readonly scopeType: ScopeType; public abstract readonly iterationScopeType: ScopeType; + /** + * This function is the only function that needs to be defined in the derived + * type. It should just return a list of all child scope types in the given + * parent scope type. + * @param iterationScope An instance of the parent scope type from which to + * return all child target scopes + * @returns A list of all child scope types in the given parent scope type + */ protected abstract getScopesInIterationScope( iterationScope: TargetScope ): TargetScope[]; @@ -42,17 +58,12 @@ export default abstract class NestedScopeHandler implements ScopeHandler { throw new NotHierarchicalScopeError(this.scopeType); } - const iterationScope = getPreferredScope( - this.iterationScopeHandler.getScopesTouchingPosition(editor, position) - ); - - if (iterationScope == null) { - return []; - } - - return this.getScopesInIterationScope(iterationScope).filter(({ domain }) => - domain.contains(position) - ); + return this.iterationScopeHandler + .getScopesTouchingPosition(editor, position) + .flatMap((iterationScope) => + this.getScopesInIterationScope(iterationScope) + ) + .filter(({ domain }) => domain.contains(position)); } getScopesOverlappingRange(editor: TextEditor, range: Range): TargetScope[] { @@ -89,6 +100,9 @@ export default abstract class NestedScopeHandler implements ScopeHandler { ): TargetScope { let remainingOffset = offset; + // Note that most of the heavy lifting is done by iterateScopeGroups; here + // we just repeatedly subtract `scopes.length` until we have seen as many + // scopes as required by `offset`. const iterator = this.iterateScopeGroups(editor, position, direction); for (const scopes of iterator) { if (scopes.length >= remainingOffset) { @@ -103,14 +117,33 @@ export default abstract class NestedScopeHandler implements ScopeHandler { throw new OutOfRangeError(); } + /** + * Yields groups of scopes for use in {@link getScopeRelativeToPosition}. + * Begins by returning a list of all scopes in the iteration scope containing + * {@link position} that are after {@link position} (before if + * {@link direction} is `"backward"`). + * + * Then repeatedly calls {@link getScopeRelativeToPosition} on the parent + * scope and returns all child scopes in each returned parent scope. + * + * @param editor The editor containing {@link position} + * @param position The position passed in to + * {@link getScopeRelativeToPosition} + * @param direction The direction passed in to + * {@link getScopeRelativeToPosition} + */ private *iterateScopeGroups( editor: TextEditor, position: Position, direction: Direction ): Generator { - const containingIterationScope = getPreferredScope( - this.iterationScopeHandler.getScopesTouchingPosition(editor, position) - ); + const containingIterationScopes = + this.iterationScopeHandler.getScopesTouchingPosition(editor, position); + + const containingIterationScope = + direction === "forward" + ? getRightScope(containingIterationScopes) + : getLeftScope(containingIterationScopes); let currentPosition = position; @@ -122,6 +155,8 @@ export default abstract class NestedScopeHandler implements ScopeHandler { : domain.end.isBeforeOrEqual(position) ); + // Move current position past containing scope so that asking for next + // parent iteration scope won't just give us back the same on currentPosition = direction === "forward" ? containingIterationScope.domain.end @@ -129,6 +164,11 @@ export default abstract class NestedScopeHandler implements ScopeHandler { } while (true) { + // Note that we always use an `offset` of 1 here. We could instead have + // left `currentPosition` unchanged and incremented offset, but in some + // cases it will be more efficient not to ask parent to walk past the same + // scopes over and over again. Eg for surrounding pair this can help us. + // For line it makes no difference. const iterationScope = this.iterationScopeHandler.getScopeRelativeToPosition( editor, @@ -139,6 +179,8 @@ export default abstract class NestedScopeHandler implements ScopeHandler { yield this.getScopesInIterationScope(iterationScope); + // Move current position past the scope we just used so that asking for next + // parent iteration scope won't just give us back the same on currentPosition = direction === "forward" ? iterationScope.domain.end diff --git a/src/test/suite/fixtures/recorded/containingScope/clearWord.yml b/src/test/suite/fixtures/recorded/containingScope/clearWord.yml new file mode 100644 index 0000000000..be16534397 --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearWord.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear word + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbb. + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + marks: {} +finalState: + documentContents: aaa. + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: word}}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearWord2.yml b/src/test/suite/fixtures/recorded/containingScope/clearWord2.yml new file mode 100644 index 0000000000..ae540c96bd --- /dev/null +++ b/src/test/suite/fixtures/recorded/containingScope/clearWord2.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear word + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: .aaaBbb + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: .Bbb + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: word}}]}] From e10a9a8b70d84be2a5f999271b1b44c9168dee03 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 20 Oct 2022 11:07:48 +0100 Subject: [PATCH 53/69] Remove `isPreferredOver` --- .../modifiers/getPreferredScope.ts | 12 ++--- .../scopeHandlers/NestedScopeHandler.ts | 1 - .../scopeHandlers/TokenScopeHandler.ts | 47 ------------------- .../modifiers/scopeHandlers/scope.types.ts | 10 ---- .../scopeHandlers/scopeHandler.types.ts | 13 ++--- 5 files changed, 8 insertions(+), 75 deletions(-) diff --git a/src/processTargets/modifiers/getPreferredScope.ts b/src/processTargets/modifiers/getPreferredScope.ts index 04475f8b30..cb9ab1fc53 100644 --- a/src/processTargets/modifiers/getPreferredScope.ts +++ b/src/processTargets/modifiers/getPreferredScope.ts @@ -2,19 +2,13 @@ import { Scope } from "./scopeHandlers/scope.types"; /** * Given a list of scopes, returns the preferred scope, or `undefined` if - * {@link scopes} is empty. If {@link Scope.isPreferredOver} is defined and - * returns a boolean, we use that preference. Otherwise we just prefer the - * rightmost scope. + * {@link scopes} is empty. The preferred scope will always be the rightmost + * scope. * @param scopes A list of scopes to choose from * @returns A single preferred scope, or `undefined` if {@link scopes} is empty */ export function getPreferredScope(scopes: T[]): T | undefined { - return getScopeHelper( - scopes, - (scope1, scope2) => - scope1.isPreferredOver?.(scope2) ?? - scope1.domain.start.isAfter(scope2.domain.start) - ); + return getRightScope(scopes); } /** diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index 30c85d11ca..c9cedf428d 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -87,7 +87,6 @@ export default abstract class NestedScopeHandler implements ScopeHandler { .map((iterationScope) => ({ domain: iterationScope.domain, editor, - isPreferredOver: iterationScope.isPreferredOver, getScopes: () => this.getScopesInIterationScope(iterationScope), })); } diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index 093729431a..9df572e71f 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -1,6 +1,4 @@ -import type { Range, TextEditor } from "vscode"; import { NestedScopeHandler } from "."; -import { getMatcher } from "../../../core/tokenizer"; import type { ScopeType } from "../../../typings/targetDescriptor.types"; import { getTokensInRange } from "../../../util/getTokensInRange"; import { TokenTarget } from "../../targets"; @@ -27,51 +25,6 @@ export default class TokenScopeHandler extends NestedScopeHandler { contentRange: range, isReversed, }), - isPreferredOver(other) { - return isPreferredOver(editor, range, other.domain); - }, })); } } - -/** - * Determines whether token {@link a} is preferred over {@link b}. - * @param editor The editor containing {@link a} and {@link b} - * @param a A token range - * @param b A token range - * @returns `true` if token {@link a} is preferred over {@link b}; `false` if - * token {@link b} is preferred over {@link a}; `undefined` otherwise - */ -function isPreferredOver( - editor: TextEditor, - a: Range, - b: Range -): boolean | undefined { - const { document } = editor; - const { identifierMatcher } = getMatcher(document.languageId); - - // If multiple matches sort and take the first - const textA = document.getText(a); - const textB = document.getText(b); - - // First sort on identifier(alphanumeric) - const aIsAlphaNum = identifierMatcher.test(textA); - const bIsAlphaNum = identifierMatcher.test(textB); - - if (aIsAlphaNum && !bIsAlphaNum) { - return true; - } - - if (bIsAlphaNum && !aIsAlphaNum) { - return false; - } - - // Second sort on length - const lengthDiff = textA.length - textB.length; - if (lengthDiff !== 0) { - return lengthDiff > 0 ? true : false; - } - - // Otherwise no preference - return undefined; -} diff --git a/src/processTargets/modifiers/scopeHandlers/scope.types.ts b/src/processTargets/modifiers/scopeHandlers/scope.types.ts index e8ca19f470..c2d36c564e 100644 --- a/src/processTargets/modifiers/scopeHandlers/scope.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scope.types.ts @@ -28,16 +28,6 @@ export interface Scope { * works from anywhere within the given class. */ readonly domain: Range; - - /** - * This fuction can be defined to indicate how to choose between adjacent - * scopes. If the input target is zero width, and between two adjacent - * scopes, this funciton will be used to decide which scope is considered to - * contain the input target. If this function is `undefined`, or returns - * `undefined`, then the one to the right will be preferred. - * @param other The scope to compare to - */ - isPreferredOver?(other: Scope): boolean | undefined; } /** diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 172e339c48..5e30a73fd3 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -58,9 +58,8 @@ export interface ScopeHandler { * scope.domain.start <= position && scope.domain.end >= position * ``` * - * If the position is directly adjacent to two scopes, return both. You can - * use {@link TargetScope.isPreferredOver} to indicate which one should have - * precedence. If no scope contains the given position, return an empty list. + * If the position is directly adjacent to two scopes, return both. If no + * scope touches the given position, return an empty list. * * Note that if this scope type is hierarchical, return only minimal scopes if * {@link ancestorIndex} is omitted or is 0. Ie if scope A and scope B both @@ -121,14 +120,12 @@ export interface ScopeHandler { * ``` * * If the position is directly adjacent to two iteration scopes, return both. - * You can use {@link TargetScope.isPreferredOver} to indicate which one - * should have precedence. If no iteration scope contains the given - * position, return an empty list. + * If no iteration scope touches the given position, return an empty list. * * Note that if the iteration scope type is hierarchical, return only minimal * iteration scopes, ie if iteration scope A and iteration scope B both touch - * {@link position}, and iteration scope A contains iteration scope B, - * return iteration scope B but not iteration scope A. + * {@link position}, and iteration scope A contains iteration scope B, return + * iteration scope B but not iteration scope A. * * @param editor The editor containing {@link position} * @param position The position from which to expand From 994b3645ce775e4fd455afc039f9067605ba4e28 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:21:44 +0100 Subject: [PATCH 54/69] Support `ancestorIndex` on api surface --- .../modifiers/ContainingScopeStage.ts | 14 +++++++++++--- src/typings/targetDescriptor.types.ts | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/processTargets/modifiers/ContainingScopeStage.ts b/src/processTargets/modifiers/ContainingScopeStage.ts index ec21df7c48..d4ef7421e7 100644 --- a/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/src/processTargets/modifiers/ContainingScopeStage.ts @@ -39,7 +39,7 @@ export class ContainingScopeStage implements ModifierStage { editor, contentRange: { start, end }, } = target; - const { scopeType } = this.modifier; + const { scopeType, ancestorIndex } = this.modifier; const scopeHandler = getScopeHandler( scopeType, @@ -50,7 +50,11 @@ export class ContainingScopeStage implements ModifierStage { return getLegacyScopeStage(this.modifier).run(context, target); } - const startScopes = scopeHandler.getScopesTouchingPosition(editor, start); + const startScopes = scopeHandler.getScopesTouchingPosition( + editor, + start, + ancestorIndex + ); if (startScopes.length === 0) { throw new NoContainingScopeError(this.modifier.scopeType.type); @@ -75,7 +79,11 @@ export class ContainingScopeStage implements ModifierStage { // between start and end scopes. For the end scope, we break ties to the // left so that the scope will have non-empty overlap with input target // content range. - const endScopes = scopeHandler.getScopesTouchingPosition(editor, end); + const endScopes = scopeHandler.getScopesTouchingPosition( + editor, + end, + ancestorIndex + ); const endScope = getLeftScope(endScopes); if (endScope == null) { diff --git a/src/typings/targetDescriptor.types.ts b/src/typings/targetDescriptor.types.ts index 52755635d6..4501562956 100644 --- a/src/typings/targetDescriptor.types.ts +++ b/src/typings/targetDescriptor.types.ts @@ -183,6 +183,7 @@ export interface ExcludeInteriorModifier { export interface ContainingScopeModifier { type: "containingScope"; scopeType: ScopeType; + ancestorIndex?: number; } export interface EveryScopeModifier { From 37268e1e1fcdca83e6f7750d38ae5f3742545ee6 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 20 Oct 2022 18:18:46 +0100 Subject: [PATCH 55/69] Improved jsdocs --- .../scopeHandlers/scopeHandler.types.ts | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 5e30a73fd3..2249032bb2 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -127,6 +127,10 @@ export interface ScopeHandler { * {@link position}, and iteration scope A contains iteration scope B, return * iteration scope B but not iteration scope A. * + * FIXME: We may want to remove this function and just call + * `iterationScope.getScopesTouchingPosition`, then run + * `getScopesOverlappingRange` on that range. + * * @param editor The editor containing {@link position} * @param position The position from which to expand */ @@ -137,14 +141,21 @@ export interface ScopeHandler { /** * Returns a scope before or after {@link position}, depending on - * {@link direction}. If {@link direction} is `"forward"`, consider all - * scopes whose {@link Scope.domain.start} is equal or after - * {@link position}. If {@link direction} is `"backward"`, consider all - * scopes whose {@link Scope.domain.end} is equal or before - * {@link position}. Note that {@link offset} will always be greater than or - * equal to 1. For example, an {@link offset} of 1 should return the first - * scope after {@link position} (before if {@link direction} is - * `"backward"`). + * {@link direction}. If {@link direction} is `"forward"` and {@link offset} + * is 1, return the leftmost scope whose {@link Scope.domain.start} is equal + * or after {@link position}. If {@link direction} is `"forward"` and + * {@link offset} is 2, return the leftmost scope whose + * {@link Scope.domain.start} is equal or after the {@link Scope.domain.end} + * of the scope at `offset` 1. Etc. + * + * If {@link direction} is `"backward"` and {@link offset} is 1, return the + * rightmost scope whose {@link Scope.domain.end} is equal or before + * {@link position}. If {@link direction} is `"backward"` and {@link offset} + * is 2, return the rightmost scope whose {@link Scope.domain.end} is equal + * or after the {@link Scope.domain.start} of the scope at `offset` 1. Etc. + * + * Note that {@link offset} will always be greater than or equal to 1. + * * @param editor The editor containing {@link position} * @param position The position from which to start * @param offset Which scope before / after position to return From f86b98ceffbaf9af8a6d821603d5e4f7c4c13ad5 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 20 Oct 2022 19:05:42 +0100 Subject: [PATCH 56/69] Split and cleanup relative stages --- src/processTargets/getModifierStage.ts | 2 +- .../modifiers/RelativeExclusiveScopeStage.ts | 84 ++++++++ .../modifiers/RelativeInclusiveScopeStage.ts | 113 +++++++++++ .../modifiers/RelativeScopeStage.ts | 185 ++---------------- 4 files changed, 210 insertions(+), 174 deletions(-) create mode 100644 src/processTargets/modifiers/RelativeExclusiveScopeStage.ts create mode 100644 src/processTargets/modifiers/RelativeInclusiveScopeStage.ts diff --git a/src/processTargets/getModifierStage.ts b/src/processTargets/getModifierStage.ts index c56fb244e5..1c0dcda855 100644 --- a/src/processTargets/getModifierStage.ts +++ b/src/processTargets/getModifierStage.ts @@ -17,7 +17,7 @@ import { OrdinalScopeStage } from "./modifiers/OrdinalScopeStage"; import PositionStage from "./modifiers/PositionStage"; import RangeModifierStage from "./modifiers/RangeModifierStage"; import RawSelectionStage from "./modifiers/RawSelectionStage"; -import { RelativeScopeStage } from "./modifiers/RelativeScopeStage"; +import RelativeScopeStage from "./modifiers/RelativeScopeStage"; import { ModifierStage } from "./PipelineStages.types"; export default (modifier: Modifier): ModifierStage => { diff --git a/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts new file mode 100644 index 0000000000..46e0248953 --- /dev/null +++ b/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -0,0 +1,84 @@ +import type { Position, TextEditor } from "vscode"; +import type { Target } from "../../typings/target.types"; +import type { RelativeScopeModifier } from "../../typings/targetDescriptor.types"; +import type { ProcessedTargetsContext } from "../../typings/Types"; +import type { ModifierStage } from "../PipelineStages.types"; +import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import { getLeftScope, getRightScope } from "./getPreferredScope"; +import { runLegacy } from "./relativeScopeLegacy"; +import getScopeHandler from "./scopeHandlers/getScopeHandler"; +import type { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; + +export default class RelativeExclusiveScopeStage implements ModifierStage { + constructor(private modifier: RelativeScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + const scopeHandler = getScopeHandler( + this.modifier.scopeType, + target.editor.document.languageId + ); + + if (scopeHandler == null) { + return runLegacy(this.modifier, context, target); + } + + const { isReversed, editor, contentRange: inputRange } = target; + const { length: desiredScopeCount, direction, offset } = this.modifier; + + const initialPosition = inputRange.isEmpty + ? getInitialPositionForEmptyInputRange( + scopeHandler, + direction, + editor, + inputRange.start + ) + : direction === "forward" + ? inputRange.end + : inputRange.start; + + const proximalScope = scopeHandler.getScopeRelativeToPosition( + editor, + initialPosition, + offset, + direction + ); + + if (desiredScopeCount === 1) { + return [proximalScope.getTarget(isReversed)]; + } + + const distalScope = scopeHandler.getScopeRelativeToPosition( + editor, + direction === "forward" + ? proximalScope.domain.end + : proximalScope.domain.start, + desiredScopeCount - 1, + direction + ); + + return [constructScopeRangeTarget(isReversed, proximalScope, distalScope)]; + } +} + +function getInitialPositionForEmptyInputRange( + scopeHandler: ScopeHandler, + direction: string, + editor: TextEditor, + inputPosition: Position +) { + const scopesTouchingPosition = scopeHandler.getScopesTouchingPosition( + editor, + inputPosition + ); + + const skipScope = + direction === "forward" + ? getRightScope(scopesTouchingPosition) + : getLeftScope(scopesTouchingPosition); + + return ( + (direction === "forward" + ? skipScope?.domain.end + : skipScope?.domain.start) ?? inputPosition + ); +} diff --git a/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts new file mode 100644 index 0000000000..020f7a6297 --- /dev/null +++ b/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts @@ -0,0 +1,113 @@ +import type { Range, TextEditor } from "vscode"; +import { NoContainingScopeError } from "../../errors"; +import type { Target } from "../../typings/target.types"; +import type { + Direction, + RelativeScopeModifier, +} from "../../typings/targetDescriptor.types"; +import type { ProcessedTargetsContext } from "../../typings/Types"; +import type { ModifierStage } from "../PipelineStages.types"; +import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import { getLeftScope, getRightScope } from "./getPreferredScope"; +import { runLegacy } from "./relativeScopeLegacy"; +import getScopeHandler from "./scopeHandlers/getScopeHandler"; +import type { TargetScope } from "./scopeHandlers/scope.types"; +import type { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; +import { TooFewScopesError } from "./TooFewScopesError"; + +export class RelativeInclusiveScopeStage implements ModifierStage { + constructor(private modifier: RelativeScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + const scopeHandler = getScopeHandler( + this.modifier.scopeType, + target.editor.document.languageId + ); + + if (scopeHandler == null) { + return runLegacy(this.modifier, context, target); + } + + const { isReversed, editor, contentRange: inputRange } = target; + const { scopeType, length: desiredScopeCount, direction } = this.modifier; + + const index0Scopes = getIndex0Scopes( + scopeHandler, + direction, + editor, + inputRange + ); + + const index0ScopeCount = index0Scopes.length; + + if (index0ScopeCount === 0) { + throw new NoContainingScopeError(scopeType.type); + } + + if (index0ScopeCount > desiredScopeCount) { + throw new TooFewScopesError( + desiredScopeCount, + index0ScopeCount, + scopeType.type + ); + } + + const proximalScope = + direction === "forward" ? index0Scopes[0] : index0Scopes.at(-1)!; + + const initialPosition = + direction === "forward" + ? index0Scopes.at(-1)!.domain.end + : index0Scopes[0].domain.start; + + const distalScope = + desiredScopeCount > index0ScopeCount + ? scopeHandler.getScopeRelativeToPosition( + editor, + initialPosition, + desiredScopeCount - index0ScopeCount, + direction + ) + : direction === "forward" + ? index0Scopes.at(-1)! + : index0Scopes[0]; + + return [constructScopeRangeTarget(isReversed, proximalScope, distalScope)]; + } +} + +/** + * Returns a list of scopes that are considered to be at relative scope index + * 0, ie "containing" / "intersecting" with the input target. If the input + * target is zero length, we return at most one scope, breaking ties by moving + * in {@link direction} if the input position is adjacent to two scopes. + * @param scopeHandler The scope handler for the given scope type + * @param direction The direction defined by the modifier + * @param editor The editor containing {@link range} + * @param range The input target range + * @returns The scopes that are considered to be at index 0, ie "containing" / "intersecting" with the input target + */ +function getIndex0Scopes( + scopeHandler: ScopeHandler, + direction: Direction, + editor: TextEditor, + range: Range +): TargetScope[] { + if (range.isEmpty) { + const inputPosition = range.start; + + const scopesTouchingPosition = scopeHandler.getScopesTouchingPosition( + editor, + inputPosition + ); + + const preferredScope = + direction === "forward" + ? getRightScope(scopesTouchingPosition) + : getLeftScope(scopesTouchingPosition); + + return preferredScope == null ? [] : [preferredScope]; + } + + return scopeHandler.getScopesOverlappingRange(editor, range); +} diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index c9a1c39c8c..005da2c7ce 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -1,181 +1,20 @@ -import type { Position, Range, TextEditor } from "vscode"; -import { NoContainingScopeError } from "../../errors"; import type { Target } from "../../typings/target.types"; import type { RelativeScopeModifier } from "../../typings/targetDescriptor.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; -import { strictlyContains } from "../../util/rangeUtils"; import type { ModifierStage } from "../PipelineStages.types"; -import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; -import { getPreferredScope } from "./getPreferredScope"; -import { runLegacy } from "./relativeScopeLegacy"; -import getScopeHandler from "./scopeHandlers/getScopeHandler"; -import type { TargetScope } from "./scopeHandlers/scope.types"; -import type { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; -import { TooFewScopesError } from "./TooFewScopesError"; - -export class RelativeScopeStage implements ModifierStage { - constructor(private modifier: RelativeScopeModifier) {} - - run(context: ProcessedTargetsContext, target: Target): Target[] { - const scopeHandler = getScopeHandler( - this.modifier.scopeType, - target.editor.document.languageId - ); - - if (scopeHandler == null) { - return runLegacy(this.modifier, context, target); - } - - return this.modifier.offset === 0 - ? this.handleIncludingIntersecting(scopeHandler, target) - : this.handleNotIncludingIntersecting(scopeHandler, target); +import RelativeExclusiveScopeStage from "./RelativeExclusiveScopeStage"; +import { RelativeInclusiveScopeStage } from "./RelativeInclusiveScopeStage"; + +export default class RelativeScopeStage implements ModifierStage { + private modiferStage: ModifierStage; + constructor(private modifier: RelativeScopeModifier) { + this.modiferStage = + this.modifier.offset === 0 + ? new RelativeInclusiveScopeStage(modifier) + : new RelativeExclusiveScopeStage(modifier); } - private handleNotIncludingIntersecting( - scopeHandler: ScopeHandler, - target: Target - ): Target[] { - const { isReversed, editor, contentRange: range } = target; - const { length: desiredScopeCount, direction, offset } = this.modifier; - - const index0Scopes = getIndex0Scopes(scopeHandler, editor, range); - - /** - * Indicates whether we move our initial position past the index 0 scopes - * before finding next scopes. The two cases we need to keep in mind are as - * follows: - * - * - For surrounding pairs, eg "round", we don't want to skip past the - * containing pair if we're in the middle of a pair. For example, in - * `(() | ())`, where `|` is our cursor, we want `"next round"` to select the - * second `()`, rather than jumping out of the big pair. - * - For tokens, we want to take into account what is considered the - * containing scope so that `"next token"` and `"token"` don't just do the - * same thing. For example, in `foo|. bar`, where `|` is the cursor, - * `"token"` is `foo`, so we want `"next token"` to refer to `.`, but in - * `.|foo bar`, `"token"` is also `foo`, so `"next token"` should refer to - * `bar`. - * - * To accommodate both of these cases, if we have only one index 0 scope, - * we ignore it if it completely contains {@link range}. That way when the - * token adjacency logic kicks in, we'll respect it, but if we're in the - * middle of a "round", we won't skip to the end. - * - */ - const skipIndex0Scopes = - index0Scopes.length > 1 || - (index0Scopes.length === 1 && - !strictlyContains(index0Scopes[0].domain, range)); - - const initialPosition = skipIndex0Scopes - ? getIndex0DistalPosition(direction, index0Scopes) - : direction === "forward" - ? range.end - : range.start; - - const proximalScope = scopeHandler.getScopeRelativeToPosition( - editor, - initialPosition, - offset, - direction - ); - - if (desiredScopeCount === 1) { - return [proximalScope.getTarget(isReversed)]; - } - - const distalScope = scopeHandler.getScopeRelativeToPosition( - editor, - direction === "forward" - ? proximalScope.domain.end - : proximalScope.domain.start, - desiredScopeCount - 1, - direction - ); - - return [constructScopeRangeTarget(isReversed, proximalScope, distalScope)]; - } - - private handleIncludingIntersecting( - scopeHandler: ScopeHandler, - target: Target - ): Target[] { - const { isReversed, editor, contentRange: range } = target; - const { scopeType, length: desiredScopeCount, direction } = this.modifier; - - const index0Scopes = getIndex0Scopes(scopeHandler, editor, range); - - const index0ScopeCount = index0Scopes.length; - - if (index0ScopeCount === 0) { - throw new NoContainingScopeError(scopeType.type); - } - - if (index0ScopeCount > desiredScopeCount) { - throw new TooFewScopesError( - desiredScopeCount, - index0ScopeCount, - scopeType.type - ); - } - - const proximalScope = - direction === "forward" ? index0Scopes[0] : index0Scopes.at(-1)!; - - const distalScope = - desiredScopeCount > index0ScopeCount - ? scopeHandler.getScopeRelativeToPosition( - editor, - getIndex0DistalPosition(direction, index0Scopes), - desiredScopeCount - index0ScopeCount, - direction - ) - : direction === "forward" - ? index0Scopes.at(-1)! - : index0Scopes[0]; - - return [constructScopeRangeTarget(isReversed, proximalScope, distalScope)]; - } -} - -/** - * Returns a position that should be considered the reference position when - * finding scopes beyond index 0. - * @param direction Which direction we're going relative, eg "forward" or "backward" - * @param index0Scopes The index 0 scopes, as defined by {@link getIndex0Scopes} - * @returns The position from which indices greater than 0 should be defined - */ -function getIndex0DistalPosition( - direction: string, - index0Scopes: TargetScope[] -): Position { - return direction === "forward" - ? index0Scopes.at(-1)!.domain.end - : index0Scopes[0].domain.start; -} - -/** - * Returns a list of scopes that are considered to be at relative scope index - * 0, ie "containing" / "intersecting" with the input target. If the input - * target is zero length, we return at most one scope: the same scope preferred - * by {@link ContainingScopeModifier}. - * @param scopeHandler The scope handler for the given scope type - * @param editor The editor containing {@link range} - * @param range The input target range - * @returns The scopes that are considered to be at index 0, ie "containing" / "intersecting" with the input target - */ -function getIndex0Scopes( - scopeHandler: ScopeHandler, - editor: TextEditor, - range: Range -): TargetScope[] { - if (range.isEmpty) { - const preferredScope = getPreferredScope( - scopeHandler.getScopesTouchingPosition(editor, range.start) - ); - - return preferredScope == null ? [] : [preferredScope]; + run(context: ProcessedTargetsContext, target: Target): Target[] { + return this.modiferStage.run(context, target); } - - return scopeHandler.getScopesOverlappingRange(editor, range); } From 307fb12a131eea1ce7d63b081ca8d1a1c922903b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 20 Oct 2022 20:08:34 +0100 Subject: [PATCH 57/69] Make scope handler constructor args optional --- .../modifiers/scopeHandlers/LineScopeHandler.ts | 4 ---- .../modifiers/scopeHandlers/NestedScopeHandler.ts | 2 +- .../modifiers/scopeHandlers/TokenScopeHandler.ts | 4 ---- .../modifiers/scopeHandlers/getScopeHandler.ts | 6 +++--- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index d7e4b1d23d..893bf927b1 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -12,10 +12,6 @@ export default class LineScopeHandler implements ScopeHandler { public readonly scopeType: ScopeType = { type: "line" }; public readonly iterationScopeType: ScopeType = { type: "document" }; - constructor(_scopeType: ScopeType, _languageId: string) { - // empty - } - getScopesTouchingPosition( editor: TextEditor, position: Position, diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index c9cedf428d..10b61e2a15 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -36,7 +36,7 @@ export default abstract class NestedScopeHandler implements ScopeHandler { private _iterationScopeHandler: ScopeHandler | undefined; - constructor(private languageId: string) {} + constructor(private languageId?: string) {} private get iterationScopeHandler(): ScopeHandler { if (this._iterationScopeHandler == null) { diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index 9df572e71f..13ee330f90 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -8,10 +8,6 @@ export default class TokenScopeHandler extends NestedScopeHandler { public readonly scopeType: ScopeType = { type: "token" }; public readonly iterationScopeType: ScopeType = { type: "line" }; - constructor(_scopeType: ScopeType, _languageId: string) { - super(_languageId); - } - protected getScopesInIterationScope({ editor, domain, diff --git a/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts index 3a19564fae..076105119d 100644 --- a/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts @@ -21,13 +21,13 @@ import type { ScopeHandler } from "./scopeHandler.types"; */ export default function getScopeHandler( scopeType: ScopeType, - _languageId: string + _languageId?: string ): ScopeHandler | undefined { switch (scopeType.type) { case "token": - return new TokenScopeHandler(scopeType, _languageId); + return new TokenScopeHandler(); case "line": - return new LineScopeHandler(scopeType, _languageId); + return new LineScopeHandler(); default: return undefined; } From f21c8f51e086e69ada28c9cdcc1e37c9902b59f7 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 20 Oct 2022 20:25:54 +0100 Subject: [PATCH 58/69] More legacy type fixes --- .../upgradeV1ToV2/upgradeV1ToV2.ts | 4 +- .../upgradeV2ToV3/targetDescriptorV2.types.ts | 37 ++++++++----------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeV1ToV2.ts b/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeV1ToV2.ts index 471d2f568e..5f716f4b84 100644 --- a/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeV1ToV2.ts +++ b/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeV1ToV2.ts @@ -1,11 +1,11 @@ import { ActionType } from "../../../actions/actions.types"; -import { SimpleScopeTypeType } from "../../../typings/targetDescriptor.types"; import { CommandV2 } from "../upgradeV2ToV3/commandV2.types"; import { ModifierV2, PartialPrimitiveTargetDescriptorV2, PartialRangeTargetDescriptorV2, PartialTargetDescriptorV2, + SimpleScopeTypeTypeV2, } from "../upgradeV2ToV3/targetDescriptorV2.types"; import { CommandV1, @@ -41,7 +41,7 @@ function upgradeModifier(modifier: ModifierV0V1): ModifierV2[] { { type: includeSiblings ? "everyScope" : "containingScope", scopeType: { - type: scopeType as SimpleScopeTypeType, + type: scopeType as SimpleScopeTypeTypeV2, }, ...rest, }, diff --git a/src/core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types.ts b/src/core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types.ts index de3cb001ba..a5777b4c43 100644 --- a/src/core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types.ts +++ b/src/core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types.ts @@ -1,32 +1,28 @@ import { HatStyleName } from "../../hatStyles"; -export interface CursorMark { +interface CursorMark { type: "cursor"; } -export interface ThatMark { +interface ThatMark { type: "that"; } -export interface SourceMark { +interface SourceMark { type: "source"; } -export interface NothingMark { +interface NothingMark { type: "nothing"; } -export interface LastCursorPositionMark { - type: "lastCursorPosition"; -} - -export interface DecoratedSymbolMark { +interface DecoratedSymbolMark { type: "decoratedSymbol"; symbolColor: HatStyleName; character: string; } -export type LineNumberType = "absolute" | "relative" | "modulo100"; +type LineNumberType = "absolute" | "relative" | "modulo100"; export interface LineNumberPositionV2 { type: LineNumberType; @@ -48,7 +44,7 @@ export type MarkV2 = | NothingMark | LineNumberMarkV2; -export type SimpleSurroundingPairName = +type SimpleSurroundingPairName = | "angleBrackets" | "backtickQuotes" | "curlyBrackets" @@ -60,15 +56,12 @@ export type SimpleSurroundingPairName = | "parentheses" | "singleQuotes" | "squareBrackets"; -export type ComplexSurroundingPairName = - | "string" - | "any" - | "collectionBoundary"; -export type SurroundingPairName = +type ComplexSurroundingPairName = "string" | "any" | "collectionBoundary"; +type SurroundingPairName = | SimpleSurroundingPairName | ComplexSurroundingPairName; -export type SimpleScopeTypeType = +export type SimpleScopeTypeTypeV2 = | "argumentOrParameter" | "anonymousFunction" | "attribute" @@ -123,17 +116,17 @@ export type SimpleScopeTypeType = | "boundedNonWhitespaceSequence" | "url"; -export interface SimpleScopeType { - type: SimpleScopeTypeType; +interface SimpleScopeType { + type: SimpleScopeTypeTypeV2; } -export interface CustomRegexScopeType { +interface CustomRegexScopeType { type: "customRegex"; regex: string; } -export type SurroundingPairDirection = "left" | "right"; -export interface SurroundingPairScopeType { +type SurroundingPairDirection = "left" | "right"; +interface SurroundingPairScopeType { type: "surroundingPair"; delimiter: SurroundingPairName; forceDirection?: SurroundingPairDirection; From 08188162a8e5b40c96dbc3c55ebb49008b606806 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 20 Oct 2022 20:26:21 +0100 Subject: [PATCH 59/69] Add `"identifier"` scope --- cursorless-talon/src/modifiers/scopes.py | 1 + .../scopeHandlers/IdentifierScopeHandler.ts | 34 +++++++++++++++++++ .../scopeHandlers/getScopeHandler.ts | 8 +++-- .../modifiers/scopeHandlers/index.ts | 2 ++ .../recorded/scopes/clearEveryIdentifier.yml | 25 ++++++++++++++ .../recorded/scopes/clearIdentifier.yml | 23 +++++++++++++ .../recorded/scopes/clearIdentifier2.yml | 19 +++++++++++ .../recorded/scopes/clearLastIdentifier.yml | 25 ++++++++++++++ src/typings/targetDescriptor.types.ts | 1 + 9 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts create mode 100644 src/test/suite/fixtures/recorded/scopes/clearEveryIdentifier.yml create mode 100644 src/test/suite/fixtures/recorded/scopes/clearIdentifier.yml create mode 100644 src/test/suite/fixtures/recorded/scopes/clearIdentifier2.yml create mode 100644 src/test/suite/fixtures/recorded/scopes/clearLastIdentifier.yml diff --git a/cursorless-talon/src/modifiers/scopes.py b/cursorless-talon/src/modifiers/scopes.py index 31313299d4..07b253fa60 100644 --- a/cursorless-talon/src/modifiers/scopes.py +++ b/cursorless-talon/src/modifiers/scopes.py @@ -57,6 +57,7 @@ # Text-based scope types "char": "character", "word": "word", + "identifier": "identifier", "block": "paragraph", "cell": "notebookCell", "file": "document", diff --git a/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts new file mode 100644 index 0000000000..5ad1417acf --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts @@ -0,0 +1,34 @@ +import { NestedScopeHandler } from "."; +import { getMatcher } from "../../../core/tokenizer"; +import type { ScopeType } from "../../../typings/targetDescriptor.types"; +import { getMatchesInRange } from "../../../util/regex"; +import { TokenTarget } from "../../targets"; +import type { TargetScope } from "./scope.types"; + +export default class IdentifierScopeHandler extends NestedScopeHandler { + public readonly scopeType: ScopeType = { type: "identifier" }; + public readonly iterationScopeType: ScopeType = { type: "line" }; + + private regex: RegExp; + + constructor(languageId: string) { + super(languageId); + this.regex = getMatcher(languageId).identifierMatcher; + } + + protected getScopesInIterationScope({ + editor, + domain, + }: TargetScope): TargetScope[] { + return getMatchesInRange(this.regex, editor, domain).map((range) => ({ + editor, + domain: range, + getTarget: (isReversed) => + new TokenTarget({ + editor, + contentRange: range, + isReversed, + }), + })); + } +} diff --git a/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts index 076105119d..1510f46a00 100644 --- a/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts @@ -1,4 +1,4 @@ -import { LineScopeHandler, TokenScopeHandler } from "."; +import { IdentifierScopeHandler, LineScopeHandler, TokenScopeHandler } from "."; import type { ScopeType } from "../../../typings/targetDescriptor.types"; import type { ScopeHandler } from "./scopeHandler.types"; @@ -13,7 +13,7 @@ import type { ScopeHandler } from "./scopeHandler.types"; * function. * * @param scopeType The scope type for which to get a scope handler - * @param _languageId The language id of the document where the scope handler + * @param languageId The language id of the document where the scope handler * will be used * @returns A scope handler for the given scope type and language id, or * undefined if the given scope type / language id combination is still using @@ -21,13 +21,15 @@ import type { ScopeHandler } from "./scopeHandler.types"; */ export default function getScopeHandler( scopeType: ScopeType, - _languageId?: string + languageId?: string ): ScopeHandler | undefined { switch (scopeType.type) { case "token": return new TokenScopeHandler(); case "line": return new LineScopeHandler(); + case "identifier": + return new IdentifierScopeHandler(languageId!); default: return undefined; } diff --git a/src/processTargets/modifiers/scopeHandlers/index.ts b/src/processTargets/modifiers/scopeHandlers/index.ts index fc7d25e0cf..0e3b135523 100644 --- a/src/processTargets/modifiers/scopeHandlers/index.ts +++ b/src/processTargets/modifiers/scopeHandlers/index.ts @@ -2,6 +2,8 @@ export * from "./NestedScopeHandler"; export { default as NestedScopeHandler } from "./NestedScopeHandler"; export * from "./LineScopeHandler"; export { default as LineScopeHandler } from "./LineScopeHandler"; +export * from "./IdentifierScopeHandler"; +export { default as IdentifierScopeHandler } from "./IdentifierScopeHandler"; export * from "./TokenScopeHandler"; export { default as TokenScopeHandler } from "./TokenScopeHandler"; export * from "./getScopeHandler"; diff --git a/src/test/suite/fixtures/recorded/scopes/clearEveryIdentifier.yml b/src/test/suite/fixtures/recorded/scopes/clearEveryIdentifier.yml new file mode 100644 index 0000000000..5aea2ede51 --- /dev/null +++ b/src/test/suite/fixtures/recorded/scopes/clearEveryIdentifier.yml @@ -0,0 +1,25 @@ +languageId: plaintext +command: + spokenForm: clear every identifier + version: 3 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: identifier} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb. + 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} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: identifier}}]}] diff --git a/src/test/suite/fixtures/recorded/scopes/clearIdentifier.yml b/src/test/suite/fixtures/recorded/scopes/clearIdentifier.yml new file mode 100644 index 0000000000..bc92deb8a1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/scopes/clearIdentifier.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear identifier + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: identifier} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: foo. + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: {} +finalState: + documentContents: . + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: identifier}}]}] diff --git a/src/test/suite/fixtures/recorded/scopes/clearIdentifier2.yml b/src/test/suite/fixtures/recorded/scopes/clearIdentifier2.yml new file mode 100644 index 0000000000..2a737e9b8a --- /dev/null +++ b/src/test/suite/fixtures/recorded/scopes/clearIdentifier2.yml @@ -0,0 +1,19 @@ +languageId: plaintext +command: + spokenForm: clear identifier + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: identifier} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: . + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: identifier}}]}] +thrownError: {name: NoContainingScopeError} diff --git a/src/test/suite/fixtures/recorded/scopes/clearLastIdentifier.yml b/src/test/suite/fixtures/recorded/scopes/clearLastIdentifier.yml new file mode 100644 index 0000000000..7c2087cd49 --- /dev/null +++ b/src/test/suite/fixtures/recorded/scopes/clearLastIdentifier.yml @@ -0,0 +1,25 @@ +languageId: plaintext +command: + spokenForm: clear last identifier + version: 3 + targets: + - type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: identifier} + start: -1 + length: 1 + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb. + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: aaa . + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: ordinalScope, scopeType: {type: identifier}, start: -1, length: 1}]}] diff --git a/src/typings/targetDescriptor.types.ts b/src/typings/targetDescriptor.types.ts index 4501562956..6a032939c7 100644 --- a/src/typings/targetDescriptor.types.ts +++ b/src/typings/targetDescriptor.types.ts @@ -128,6 +128,7 @@ export type SimpleScopeTypeType = | "document" | "character" | "word" + | "identifier" | "nonWhitespaceSequence" | "boundedNonWhitespaceSequence" | "url"; From 32cb685420f9906bbb639e27c190aff5d7570efa Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 21 Oct 2022 15:34:01 +0100 Subject: [PATCH 60/69] Working new code --- .../src/modifiers/relative_scope.py | 14 +- src/core/TokenGraphemeSplitter.ts | 2 +- .../modifiers/getLegacyScopeStage.ts | 9 +- .../scopeHandlers/CharacterScopeHandler.ts | 28 +++ .../scopeHandlers/IdentifierScopeHandler.ts | 12 +- .../scopeHandlers/LineScopeHandler.ts | 10 +- .../scopeHandlers/NestedScopeHandler.ts | 6 +- .../scopeHandlers/WordScopeHandler.ts | 86 ++++++++ .../modifiers/scopeHandlers/WordTokenizer.ts | 22 ++ .../scopeHandlers/getScopeHandler.ts | 22 +- .../modifiers/scopeHandlers/index.ts | 4 + .../scopeTypeStages/SubTokenStages.ts | 207 ------------------ src/processTargets/modifiers/subToken.ts | 30 --- .../containingScope/clearTwoTokens.yml | 6 +- .../clearTwoTokensBackward4.yml | 6 +- .../recorded/inference/bringOddToToken.yml | 2 +- .../recorded/inference/takeOddPastToken.yml | 2 +- .../relativeScopes/clearTokenBackward.yml | 26 +++ .../selectionTypes/clearPreviousWord.yml | 6 +- .../recorded/subtoken/clearNextWord.yml | 26 +++ .../recorded/subtoken/clearSixthNextWord.yml | 30 +++ .../recorded/subtoken/ditchFourthWordLine.yml | 27 +++ .../recorded/subtoken/ditchLastWordLine.yml | 27 +++ .../recorded/subtoken/ditchThirdWordLine.yml | 27 +++ .../recorded/subtoken/takeEveryWordLine2.yml | 35 +++ .../recorded/subtoken/takeFirstChar2.yml | 4 +- .../recorded/subtoken/takeFirstChar3.yml | 4 +- src/test/suite/fixtures/subtoken.fixture.ts | 4 - src/test/suite/subtoken.test.ts | 5 +- src/util/regex.ts | 4 +- 30 files changed, 405 insertions(+), 288 deletions(-) create mode 100644 src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts create mode 100644 src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts create mode 100644 src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts delete mode 100644 src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts delete mode 100644 src/processTargets/modifiers/subToken.ts create mode 100644 src/test/suite/fixtures/recorded/relativeScopes/clearTokenBackward.yml create mode 100644 src/test/suite/fixtures/recorded/subtoken/clearNextWord.yml create mode 100644 src/test/suite/fixtures/recorded/subtoken/clearSixthNextWord.yml create mode 100644 src/test/suite/fixtures/recorded/subtoken/ditchFourthWordLine.yml create mode 100644 src/test/suite/fixtures/recorded/subtoken/ditchLastWordLine.yml create mode 100644 src/test/suite/fixtures/recorded/subtoken/ditchThirdWordLine.yml create mode 100644 src/test/suite/fixtures/recorded/subtoken/takeEveryWordLine2.yml diff --git a/cursorless-talon/src/modifiers/relative_scope.py b/cursorless-talon/src/modifiers/relative_scope.py index b1037021f7..126c9d0887 100644 --- a/cursorless-talon/src/modifiers/relative_scope.py +++ b/cursorless-talon/src/modifiers/relative_scope.py @@ -56,11 +56,23 @@ def cursorless_relative_scope_count(m) -> dict[str, Any]: ) +@mod.capture(rule=" {user.cursorless_backward_modifier}") +def cursorless_relative_scope_one_backward(m) -> dict[str, Any]: + """Take scope backward, eg `funk backward`""" + return create_relative_scope_modifier( + m.cursorless_scope_type, + 0, + 1, + m.cursorless_backward_modifier, + ) + + @mod.capture( rule=( " | " " | " - "" + " | " + "" ) ) def cursorless_relative_scope(m) -> dict[str, Any]: diff --git a/src/core/TokenGraphemeSplitter.ts b/src/core/TokenGraphemeSplitter.ts index dbfd661394..6e579bbbb9 100644 --- a/src/core/TokenGraphemeSplitter.ts +++ b/src/core/TokenGraphemeSplitter.ts @@ -191,7 +191,7 @@ export class TokenGraphemeSplitter { } } -export interface Grapheme { +export interface Grapheme { /** The normalised text of the grapheme. */ text: string; diff --git a/src/processTargets/modifiers/getLegacyScopeStage.ts b/src/processTargets/modifiers/getLegacyScopeStage.ts index f3b32e9d91..fa219279ca 100644 --- a/src/processTargets/modifiers/getLegacyScopeStage.ts +++ b/src/processTargets/modifiers/getLegacyScopeStage.ts @@ -1,9 +1,9 @@ -import { +import type { ContainingScopeModifier, EveryScopeModifier, SurroundingPairModifier, } from "../../typings/targetDescriptor.types"; -import { ModifierStage } from "../PipelineStages.types"; +import type { ModifierStage } from "../PipelineStages.types"; import ItemStage from "./ItemStage"; import BoundedNonWhitespaceSequenceStage from "./scopeTypeStages/BoundedNonWhitespaceStage"; import ContainingSyntaxScopeStage, { @@ -19,7 +19,6 @@ import { NonWhitespaceSequenceStage, UrlStage, } from "./scopeTypeStages/RegexStage"; -import { CharacterStage, WordStage } from "./scopeTypeStages/SubTokenStages"; import SurroundingPairStage from "./SurroundingPairStage"; /** @@ -55,10 +54,6 @@ export default function getLegacyScopeStage( return new ItemStage(modifier); case "customRegex": return new CustomRegexStage(modifier as CustomRegexModifier); - case "word": - return new WordStage(modifier); - case "character": - return new CharacterStage(modifier); case "surroundingPair": return new SurroundingPairStage(modifier as SurroundingPairModifier); default: diff --git a/src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts new file mode 100644 index 0000000000..5b012f7281 --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts @@ -0,0 +1,28 @@ +import { NestedScopeHandler } from "."; +import { GRAPHEME_SPLIT_REGEX } from "../../../core/TokenGraphemeSplitter"; +import { getMatchesInRange } from "../../../util/regex"; +import { PlainTarget } from "../../targets"; +import type { TargetScope } from "./scope.types"; + +export default class CharacterScopeHandler extends NestedScopeHandler { + public readonly scopeType = { type: "character" } as const; + public readonly iterationScopeType = { type: "token" } as const; + + protected getScopesInIterationScope({ + editor, + domain, + }: TargetScope): TargetScope[] { + return getMatchesInRange(GRAPHEME_SPLIT_REGEX, editor, domain).map( + (range) => ({ + editor, + domain: range, + getTarget: (isReversed) => + new PlainTarget({ + editor, + contentRange: range, + isReversed, + }), + }) + ); + } +} diff --git a/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts index 5ad1417acf..0117167498 100644 --- a/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts @@ -1,20 +1,14 @@ import { NestedScopeHandler } from "."; import { getMatcher } from "../../../core/tokenizer"; -import type { ScopeType } from "../../../typings/targetDescriptor.types"; import { getMatchesInRange } from "../../../util/regex"; import { TokenTarget } from "../../targets"; import type { TargetScope } from "./scope.types"; export default class IdentifierScopeHandler extends NestedScopeHandler { - public readonly scopeType: ScopeType = { type: "identifier" }; - public readonly iterationScopeType: ScopeType = { type: "line" }; + public readonly scopeType = { type: "identifier" } as const; + public readonly iterationScopeType = { type: "line" } as const; - private regex: RegExp; - - constructor(languageId: string) { - super(languageId); - this.regex = getMatcher(languageId).identifierMatcher; - } + private regex: RegExp = getMatcher(this.languageId).identifierMatcher; protected getScopesInIterationScope({ editor, diff --git a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index 893bf927b1..7e9a277707 100644 --- a/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -1,6 +1,6 @@ import { range } from "lodash"; import { Position, Range, TextEditor } from "vscode"; -import { Direction, ScopeType } from "../../../typings/targetDescriptor.types"; +import { Direction } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { LineTarget } from "../../targets"; import { OutOfRangeError } from "../targetSequenceUtils"; @@ -9,8 +9,12 @@ import type { IterationScope, TargetScope } from "./scope.types"; import type { ScopeHandler } from "./scopeHandler.types"; export default class LineScopeHandler implements ScopeHandler { - public readonly scopeType: ScopeType = { type: "line" }; - public readonly iterationScopeType: ScopeType = { type: "document" }; + public readonly iterationScopeType = { type: "document" } as const; + + constructor( + public readonly scopeType: { type: "line" }, + protected languageId: string + ) {} getScopesTouchingPosition( editor: TextEditor, diff --git a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index 10b61e2a15..6e8c56b0ee 100644 --- a/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -19,7 +19,6 @@ import type { ScopeHandler } from "./scopeHandler.types"; * regex matches to this base class and let it handle the rest. */ export default abstract class NestedScopeHandler implements ScopeHandler { - public abstract readonly scopeType: ScopeType; public abstract readonly iterationScopeType: ScopeType; /** @@ -36,7 +35,10 @@ export default abstract class NestedScopeHandler implements ScopeHandler { private _iterationScopeHandler: ScopeHandler | undefined; - constructor(private languageId?: string) {} + constructor( + public readonly scopeType: ScopeType, + protected languageId: string + ) {} private get iterationScopeHandler(): ScopeHandler { if (this._iterationScopeHandler == null) { diff --git a/src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts new file mode 100644 index 0000000000..81783e0386 --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts @@ -0,0 +1,86 @@ +import { Range, TextEditor } from "vscode"; +import { NestedScopeHandler } from "."; +import { SubTokenWordTarget } from "../../targets"; +import type { TargetScope } from "./scope.types"; +import WordTokenizer from "./WordTokenizer"; + +export default class WordScopeHandler extends NestedScopeHandler { + public readonly scopeType = { type: "word" } as const; + public readonly iterationScopeType = { type: "identifier" } as const; + + private wordTokenizer = new WordTokenizer(this.languageId); + + protected getScopesInIterationScope({ + editor, + domain, + }: TargetScope): TargetScope[] { + const { document } = editor; + // FIXME: Switch to using getMatchesInRange once we are able to properly + // mock away vscode for the unit tests in subtoken.test.ts + const offset = document.offsetAt(domain.start); + const matches = this.wordTokenizer.splitIdentifier( + document.getText(domain) + ); + const contentRanges = matches.map( + (match) => + new Range( + document.positionAt(offset + match.index), + document.positionAt(offset + match.index + match.text.length) + ) + ); + + return contentRanges.map((range, i) => ({ + editor, + domain: range, + getTarget: (isReversed) => { + const previousContentRange = i > 0 ? contentRanges[i - 1] : null; + const nextContentRange = + i + 1 < contentRanges.length ? contentRanges[i + 1] : null; + + return constructTarget( + isReversed, + editor, + previousContentRange, + range, + nextContentRange + ); + }, + })); + } +} + +function constructTarget( + isReversed: boolean, + editor: TextEditor, + previousContentRange: Range | null, + contentRange: Range, + nextContentRange: Range | null +) { + const leadingDelimiterRange = + previousContentRange != null && + contentRange.start.isAfter(previousContentRange.end) + ? new Range(previousContentRange.end, contentRange.start) + : undefined; + + const trailingDelimiterRange = + nextContentRange != null && nextContentRange.start.isAfter(contentRange.end) + ? new Range(contentRange.end, nextContentRange.start) + : undefined; + + const isInDelimitedList = + leadingDelimiterRange != null || trailingDelimiterRange != null; + const insertionDelimiter = isInDelimitedList + ? editor.document.getText( + (leadingDelimiterRange ?? trailingDelimiterRange)! + ) + : ""; + + return new SubTokenWordTarget({ + editor, + isReversed, + contentRange, + insertionDelimiter, + leadingDelimiterRange, + trailingDelimiterRange, + }); +} diff --git a/src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts b/src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts new file mode 100644 index 0000000000..3b3b2960df --- /dev/null +++ b/src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts @@ -0,0 +1,22 @@ +import { getMatcher } from "../../../core/tokenizer"; +import { matchText } from "../../../util/regex"; + +const CAMEL_REGEX = /\p{Lu}?\p{Ll}+|\p{Lu}+(?!\p{Ll})|\p{N}+/gu; + +export default class WordTokenizer { + private wordRegex: RegExp; + + constructor(languageId: string) { + this.wordRegex = getMatcher(languageId).wordMatcher; + } + + public splitIdentifier(text: string) { + // First try to split on non letter characters + const wordMatches = matchText(text, this.wordRegex); + + return wordMatches.length > 1 + ? wordMatches + : // Secondly try split on camel case + matchText(text, CAMEL_REGEX); + } +} diff --git a/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts index 1510f46a00..c3136fcb8c 100644 --- a/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts @@ -1,4 +1,10 @@ -import { IdentifierScopeHandler, LineScopeHandler, TokenScopeHandler } from "."; +import { + CharacterScopeHandler, + IdentifierScopeHandler, + LineScopeHandler, + TokenScopeHandler, + WordScopeHandler, +} from "."; import type { ScopeType } from "../../../typings/targetDescriptor.types"; import type { ScopeHandler } from "./scopeHandler.types"; @@ -21,15 +27,19 @@ import type { ScopeHandler } from "./scopeHandler.types"; */ export default function getScopeHandler( scopeType: ScopeType, - languageId?: string + languageId: string ): ScopeHandler | undefined { switch (scopeType.type) { + case "character": + return new CharacterScopeHandler(scopeType, languageId); + case "word": + return new WordScopeHandler(scopeType, languageId); case "token": - return new TokenScopeHandler(); - case "line": - return new LineScopeHandler(); + return new TokenScopeHandler(scopeType, languageId); case "identifier": - return new IdentifierScopeHandler(languageId!); + return new IdentifierScopeHandler(scopeType, languageId); + case "line": + return new LineScopeHandler(scopeType as { type: "line" }, languageId); default: return undefined; } diff --git a/src/processTargets/modifiers/scopeHandlers/index.ts b/src/processTargets/modifiers/scopeHandlers/index.ts index 0e3b135523..52e5a4ac42 100644 --- a/src/processTargets/modifiers/scopeHandlers/index.ts +++ b/src/processTargets/modifiers/scopeHandlers/index.ts @@ -4,6 +4,10 @@ export * from "./LineScopeHandler"; export { default as LineScopeHandler } from "./LineScopeHandler"; export * from "./IdentifierScopeHandler"; export { default as IdentifierScopeHandler } from "./IdentifierScopeHandler"; +export * from "./CharacterScopeHandler"; +export { default as CharacterScopeHandler } from "./CharacterScopeHandler"; +export * from "./WordScopeHandler"; +export { default as WordScopeHandler } from "./WordScopeHandler"; export * from "./TokenScopeHandler"; export { default as TokenScopeHandler } from "./TokenScopeHandler"; export * from "./getScopeHandler"; diff --git a/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts b/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts deleted file mode 100644 index 0c5790b990..0000000000 --- a/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { Range, TextEditor } from "vscode"; -import { GRAPHEME_SPLIT_REGEX } from "../../../core/TokenGraphemeSplitter"; -import { NoContainingScopeError } from "../../../errors"; -import { Target } from "../../../typings/target.types"; -import { - ContainingScopeModifier, - EveryScopeModifier, -} from "../../../typings/targetDescriptor.types"; -import { ProcessedTargetsContext } from "../../../typings/Types"; -import { MatchedText, matchText } from "../../../util/regex"; -import getModifierStage from "../../getModifierStage"; -import { ModifierStage } from "../../PipelineStages.types"; -import { PlainTarget, SubTokenWordTarget } from "../../targets"; - -import { subWordSplitter } from "../subToken"; - -abstract class SubTokenStage implements ModifierStage { - constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} - - run(context: ProcessedTargetsContext, target: Target): Target[] { - const tokenStage = getModifierStage({ - type: "containingScope", - scopeType: { type: "token" }, - }); - const tokenTarget = tokenStage.run(context, target)[0]; - const { document } = target.editor; - const tokenRange = tokenTarget.contentRange; - const text = tokenTarget.contentText; - const offset = document.offsetAt(tokenRange.start); - const matches = this.getMatchedText(text, document.languageId); - const contentRanges = matches.map( - (match) => - new Range( - document.positionAt(offset + match.index), - document.positionAt(offset + match.index + match.text.length) - ) - ); - - const targets = this.createTargetsFromRanges( - target.isReversed, - target.editor, - contentRanges - ); - - // If target has explicit range filter to scopes in that range. Otherwise expand to all scopes in iteration scope. - const filteredTargets = target.hasExplicitRange - ? filterTargets(target, targets) - : targets; - - if (filteredTargets.length === 0) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - if (this.modifier.type === "everyScope") { - return filteredTargets; - } - - return [this.getSingleTarget(target, filteredTargets)]; - } - - /** - * Constructs a single range target containing all targets from - * {@link allTargets} that intersect with {@link inputTarget}. - * @param inputTarget The input target to this stage - * @param allTargets A list of all targets under consideration - * @returns A single target constructed by forming a range containing all - * targets that intersect with {@link inputTarget} - */ - private getSingleTarget(inputTarget: Target, allTargets: Target[]): Target { - let intersectingTargets = allTargets - .map((t) => ({ - target: t, - intersection: t.contentRange.intersection(inputTarget.contentRange), - })) - .filter((it) => it.intersection != null); - - if (intersectingTargets.length === 0) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - // Empty range utilize single adjacent target to the right of {@link inputTarget} - if (inputTarget.contentRange.isEmpty) { - return intersectingTargets.at(-1)!.target; - } - - // On non empty input range, utilize all targets with a non-empty - // intersection with {@link inputTarget} - intersectingTargets = intersectingTargets.filter( - (it) => !it.intersection!.isEmpty - ); - - if (intersectingTargets.length === 0) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - if (intersectingTargets.length === 1) { - return intersectingTargets[0].target; - } - - return intersectingTargets[0].target.createContinuousRangeTarget( - inputTarget.isReversed, - intersectingTargets.at(-1)!.target, - true, - true - ); - } - - /** - * Return matches for {@link text} - */ - protected abstract getMatchedText( - text: string, - languageId: string - ): MatchedText[]; - - /** - * Create one target for each element of {@link contentRanges} - */ - protected abstract createTargetsFromRanges( - isReversed: boolean, - editor: TextEditor, - contentRanges: Range[] - ): Target[]; -} - -export class WordStage extends SubTokenStage { - constructor(modifier: ContainingScopeModifier | EveryScopeModifier) { - super(modifier); - } - - protected getMatchedText(text: string, languageId: string): MatchedText[] { - return subWordSplitter(text, languageId); - } - - protected createTargetsFromRanges( - isReversed: boolean, - editor: TextEditor, - contentRanges: Range[] - ): Target[] { - return contentRanges.map((contentRange, i) => { - const previousContentRange = i > 0 ? contentRanges[i - 1] : null; - const nextContentRange = - i + 1 < contentRanges.length ? contentRanges[i + 1] : null; - - const leadingDelimiterRange = - previousContentRange != null && - contentRange.start.isAfter(previousContentRange.end) - ? new Range(previousContentRange.end, contentRange.start) - : undefined; - - const trailingDelimiterRange = - nextContentRange != null && - nextContentRange.start.isAfter(contentRange.end) - ? new Range(contentRange.end, nextContentRange.start) - : undefined; - - const isInDelimitedList = - leadingDelimiterRange != null || trailingDelimiterRange != null; - const insertionDelimiter = isInDelimitedList - ? editor.document.getText( - (leadingDelimiterRange ?? trailingDelimiterRange)! - ) - : ""; - - return new SubTokenWordTarget({ - editor, - isReversed, - contentRange, - insertionDelimiter, - leadingDelimiterRange, - trailingDelimiterRange, - }); - }); - } -} - -export class CharacterStage extends SubTokenStage { - constructor(modifier: ContainingScopeModifier | EveryScopeModifier) { - super(modifier); - } - - protected getMatchedText(text: string): MatchedText[] { - return matchText(text, GRAPHEME_SPLIT_REGEX); - } - - protected createTargetsFromRanges( - isReversed: boolean, - editor: TextEditor, - contentRanges: Range[] - ): Target[] { - return contentRanges.map( - (contentRange) => - new PlainTarget({ - editor, - isReversed, - contentRange, - }) - ); - } -} - -function filterTargets(target: Target, targets: Target[]): Target[] { - return targets.filter((t) => { - const intersection = t.contentRange.intersection(target.contentRange); - return intersection != null && !intersection.isEmpty; - }); -} diff --git a/src/processTargets/modifiers/subToken.ts b/src/processTargets/modifiers/subToken.ts deleted file mode 100644 index 137ea7d14e..0000000000 --- a/src/processTargets/modifiers/subToken.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { getMatcher } from "../../core/tokenizer"; -import { matchText } from "../../util/regex"; - -const camelRegex = /\p{Lu}?\p{Ll}+|\p{Lu}+(?!\p{Ll})|\p{N}+/gu; - -export function subWordSplitter(text: string, languageId: string) { - // First split on identifiers. The input text can contain multiple - // tokens/identifiers and these can have different formats. - // eg `publicApiV1 public_api_v1` - const { identifierMatcher, wordMatcher } = getMatcher(languageId); - return matchText(text, identifierMatcher).flatMap((t) => - splitIdentifier(wordMatcher, t.text, t.index) - ); -} - -function splitIdentifier(wordMatcher: RegExp, text: string, index: number) { - // First try to split on non letter characters - const wordMatches = matchText(text, wordMatcher); - - const matches = - wordMatches.length > 1 - ? wordMatches - : // Secondly try split on camel case - matchText(text, camelRegex); - - return matches.map((match) => ({ - index: index + match.index, - text: match.text, - })); -} diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml index 0b26709ef3..8eca8b9f64 100644 --- a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokens.yml @@ -19,8 +19,8 @@ initialState: active: {line: 0, character: 7} marks: {} finalState: - documentContents: aaa ccc + documentContents: aaa bbb selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} + - anchor: {line: 0, character: 7} + active: {line: 0, character: 7} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward4.yml b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward4.yml index 19c02334c3..a2dde7c21c 100644 --- a/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward4.yml +++ b/src/test/suite/fixtures/recorded/containingScope/clearTwoTokensBackward4.yml @@ -19,8 +19,8 @@ initialState: active: {line: 0, character: 5} marks: {} finalState: - documentContents: aaa ccc + documentContents: bbb ccc selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/inference/bringOddToToken.yml b/src/test/suite/fixtures/recorded/inference/bringOddToToken.yml index ae24220ad5..0744705166 100644 --- a/src/test/suite/fixtures/recorded/inference/bringOddToToken.yml +++ b/src/test/suite/fixtures/recorded/inference/bringOddToToken.yml @@ -23,7 +23,7 @@ finalState: documentContents: |- const foo = "hello"; - const bar = "const"; + const bar = "helloconst; selections: - anchor: {line: 2, character: 18} active: {line: 2, character: 18} diff --git a/src/test/suite/fixtures/recorded/inference/takeOddPastToken.yml b/src/test/suite/fixtures/recorded/inference/takeOddPastToken.yml index 11ae3d529b..c50d76304b 100644 --- a/src/test/suite/fixtures/recorded/inference/takeOddPastToken.yml +++ b/src/test/suite/fixtures/recorded/inference/takeOddPastToken.yml @@ -30,5 +30,5 @@ finalState: const bar = "hello"; selections: - anchor: {line: 0, character: 0} - active: {line: 2, character: 18} + active: {line: 2, character: 19} fullTargets: [{type: range, excludeAnchor: false, excludeActive: false, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: o}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: identity}}, active: {type: primitive, mark: {type: cursorToken}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: identity}}}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearTokenBackward.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearTokenBackward.yml new file mode 100644 index 0000000000..2d16e87dc1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearTokenBackward.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear token backward + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 1 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: foo. + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: {} +finalState: + documentContents: . + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 1, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord.yml index 5ae206e97a..86ac4e3e80 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord.yml @@ -19,8 +19,8 @@ initialState: active: {line: 0, character: 6} marks: {} finalState: - documentContents: aaaCccDdd + documentContents: BbbCccDdd selections: - - anchor: {line: 0, character: 3} - active: {line: 0, character: 3} + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: word}, offset: 1, length: 1, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/subtoken/clearNextWord.yml b/src/test/suite/fixtures/recorded/subtoken/clearNextWord.yml new file mode 100644 index 0000000000..9bd7d369e3 --- /dev/null +++ b/src/test/suite/fixtures/recorded/subtoken/clearNextWord.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear next word + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: word} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbbCcc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: aaa Ccc + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: word}, offset: 1, length: 1, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/subtoken/clearSixthNextWord.yml b/src/test/suite/fixtures/recorded/subtoken/clearSixthNextWord.yml new file mode 100644 index 0000000000..6ea057dd47 --- /dev/null +++ b/src/test/suite/fixtures/recorded/subtoken/clearSixthNextWord.yml @@ -0,0 +1,30 @@ +languageId: plaintext +command: + spokenForm: clear sixth next word + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: word} + offset: 6 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + aaaBbb cccDdd + eeeFff gggHhh + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |- + aaaBbb cccDdd + eeeFff Hhh + selections: + - anchor: {line: 1, character: 7} + active: {line: 1, character: 7} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: word}, offset: 6, length: 1, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/subtoken/ditchFourthWordLine.yml b/src/test/suite/fixtures/recorded/subtoken/ditchFourthWordLine.yml new file mode 100644 index 0000000000..5b1199af9f --- /dev/null +++ b/src/test/suite/fixtures/recorded/subtoken/ditchFourthWordLine.yml @@ -0,0 +1,27 @@ +languageId: plaintext +command: + spokenForm: ditch fourth word line + version: 3 + targets: + - type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: word} + start: 3 + length: 1 + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: apiV1 api_v_1 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: apiV1 v_1 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: ordinalScope, scopeType: {type: word}, start: 3, length: 1}, {type: containingScope, scopeType: {type: line}}]}] diff --git a/src/test/suite/fixtures/recorded/subtoken/ditchLastWordLine.yml b/src/test/suite/fixtures/recorded/subtoken/ditchLastWordLine.yml new file mode 100644 index 0000000000..1490b89bb0 --- /dev/null +++ b/src/test/suite/fixtures/recorded/subtoken/ditchLastWordLine.yml @@ -0,0 +1,27 @@ +languageId: plaintext +command: + spokenForm: ditch last word line + version: 3 + targets: + - type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: word} + start: -1 + length: 1 + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: apiV1 api_v_1 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: apiV1 api_v + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: ordinalScope, scopeType: {type: word}, start: -1, length: 1}, {type: containingScope, scopeType: {type: line}}]}] diff --git a/src/test/suite/fixtures/recorded/subtoken/ditchThirdWordLine.yml b/src/test/suite/fixtures/recorded/subtoken/ditchThirdWordLine.yml new file mode 100644 index 0000000000..664e704986 --- /dev/null +++ b/src/test/suite/fixtures/recorded/subtoken/ditchThirdWordLine.yml @@ -0,0 +1,27 @@ +languageId: plaintext +command: + spokenForm: ditch third word line + version: 3 + targets: + - type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: word} + start: 2 + length: 1 + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: apiV1 api_v_1 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: apiV api_v_1 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: ordinalScope, scopeType: {type: word}, start: 2, length: 1}, {type: containingScope, scopeType: {type: line}}]}] diff --git a/src/test/suite/fixtures/recorded/subtoken/takeEveryWordLine2.yml b/src/test/suite/fixtures/recorded/subtoken/takeEveryWordLine2.yml new file mode 100644 index 0000000000..8fb7498caa --- /dev/null +++ b/src/test/suite/fixtures/recorded/subtoken/takeEveryWordLine2.yml @@ -0,0 +1,35 @@ +languageId: plaintext +command: + spokenForm: take every word line + version: 3 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: word} + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: apiV1 api_v_1 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: apiV1 api_v_1 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 3} + - anchor: {line: 0, character: 3} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 5} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 9} + - anchor: {line: 0, character: 10} + active: {line: 0, character: 11} + - anchor: {line: 0, character: 12} + active: {line: 0, character: 13} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: word}}, {type: containingScope, scopeType: {type: line}}]}] diff --git a/src/test/suite/fixtures/recorded/subtoken/takeFirstChar2.yml b/src/test/suite/fixtures/recorded/subtoken/takeFirstChar2.yml index 823664f87d..66f881d46a 100644 --- a/src/test/suite/fixtures/recorded/subtoken/takeFirstChar2.yml +++ b/src/test/suite/fixtures/recorded/subtoken/takeFirstChar2.yml @@ -16,6 +16,6 @@ initialState: finalState: documentContents: aa// selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 1} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 3} fullTargets: [{type: primitive, mark: {type: cursorToken}, selectionType: token, position: contents, modifier: {type: subpiece, pieceType: character, anchor: 0, active: 0}, insideOutsideType: inside}] diff --git a/src/test/suite/fixtures/recorded/subtoken/takeFirstChar3.yml b/src/test/suite/fixtures/recorded/subtoken/takeFirstChar3.yml index 296241eef3..ebe46c8926 100644 --- a/src/test/suite/fixtures/recorded/subtoken/takeFirstChar3.yml +++ b/src/test/suite/fixtures/recorded/subtoken/takeFirstChar3.yml @@ -16,6 +16,6 @@ initialState: finalState: documentContents: ///** selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 1} + - anchor: {line: 0, character: 3} + active: {line: 0, character: 4} fullTargets: [{type: primitive, mark: {type: cursorToken}, selectionType: token, position: contents, modifier: {type: subpiece, pieceType: character, anchor: 0, active: 0}, insideOutsideType: inside}] diff --git a/src/test/suite/fixtures/subtoken.fixture.ts b/src/test/suite/fixtures/subtoken.fixture.ts index 370ffea282..7916fa3baf 100644 --- a/src/test/suite/fixtures/subtoken.fixture.ts +++ b/src/test/suite/fixtures/subtoken.fixture.ts @@ -80,10 +80,6 @@ export const subtokenFixture: Fixture[] = [ input: "aaBbÄä", expectedOutput: ["aa", "Bb", "Ää"], }, - { - input: "apiV1 api_v_1", - expectedOutput: ["api", "V", "1", "api", "v", "1"], - }, { input: "_quickBrownFox_", expectedOutput: ["quick", "Brown", "Fox"], diff --git a/src/test/suite/subtoken.test.ts b/src/test/suite/subtoken.test.ts index 235ad56ec7..7cf7450f28 100644 --- a/src/test/suite/subtoken.test.ts +++ b/src/test/suite/subtoken.test.ts @@ -1,12 +1,13 @@ import * as assert from "assert"; -import { subWordSplitter } from "../../processTargets/modifiers/subToken"; +import WordTokenizer from "../../processTargets/modifiers/scopeHandlers/WordTokenizer"; import { subtokenFixture } from "./fixtures/subtoken.fixture"; suite("subtoken regex matcher", () => { + const wordTokenizer = new WordTokenizer("anyLang"); subtokenFixture.forEach(({ input, expectedOutput }) => { test(input, () => { assert.deepStrictEqual( - subWordSplitter(input, "anyLang").map(({ text }) => text), + wordTokenizer.splitIdentifier(input).map(({ text }) => text), expectedOutput ); }); diff --git a/src/util/regex.ts b/src/util/regex.ts index dd33e14b31..c74a635fa8 100644 --- a/src/util/regex.ts +++ b/src/util/regex.ts @@ -64,7 +64,9 @@ export function getMatchesInRange( const offset = editor.document.offsetAt(range.start); const text = editor.document.getText(range); - return [...text.matchAll(regex)].map( + return matchAll( + text, + regex, (match) => new Range( editor.document.positionAt(offset + match.index!), From 4cc83afa299276b2773032125243121c0e536767 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 21 Oct 2022 14:34:44 +0000 Subject: [PATCH 61/69] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/core/TokenGraphemeSplitter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/TokenGraphemeSplitter.ts b/src/core/TokenGraphemeSplitter.ts index 6e579bbbb9..dbfd661394 100644 --- a/src/core/TokenGraphemeSplitter.ts +++ b/src/core/TokenGraphemeSplitter.ts @@ -191,7 +191,7 @@ export class TokenGraphemeSplitter { } } -export interface Grapheme { +export interface Grapheme { /** The normalised text of the grapheme. */ text: string; From 4d3ed1f06307539a24e53b893fa8346d5d57b788 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 21 Oct 2022 15:52:36 +0100 Subject: [PATCH 62/69] Docs + cheatsheet --- .../src/cheatsheet_html/sections/modifiers.py | 4 ++++ docs/user/README.md | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/cursorless-talon/src/cheatsheet_html/sections/modifiers.py b/cursorless-talon/src/cheatsheet_html/sections/modifiers.py index 2963cef3d4..fb93ad3409 100644 --- a/cursorless-talon/src/cheatsheet_html/sections/modifiers.py +++ b/cursorless-talon/src/cheatsheet_html/sections/modifiers.py @@ -121,6 +121,10 @@ def get_modifiers(): "spokenForm": f"{complex_modifiers['previous']} s", "description": "previous instances of ", }, + { + "spokenForm": f" {complex_modifiers['backward']}", + "description": "single instance of including target, going backwards", + }, { "spokenForm": f" s {complex_modifiers['backward']}", "description": " instances of including target, going backwards", diff --git a/docs/user/README.md b/docs/user/README.md index 70d0fd7336..1f3a612814 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -281,6 +281,16 @@ The `"token"` modifier expands its input to the nearest containing token. This m - `"take token"` - `"chuck token"` +##### `"identifier` + +The `"identifier"` modifier behaves like `"token"`, but only considers tokens that are viable identifiers. For example `"identifier"` could be used to select `foo`, `fooBar`, or `foo_bar`, but not `.`, `=`, `+=`, etc. For example: + +- `"copy identifier"` +- `"take identifier"` +- `"chuck identifier"` + +This scope type is useful with ordinals, allow + ##### `"paint"` Both of the commands below will expand from the mark forward and backward to include all adjacent non-whitespace characters. From aa2a74faeb9e6b5045c0d31a12cde090879ec356 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 21 Oct 2022 15:56:10 +0100 Subject: [PATCH 63/69] Update docs --- docs/user/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/README.md b/docs/user/README.md index 1f3a612814..191278e406 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -289,7 +289,7 @@ The `"identifier"` modifier behaves like `"token"`, but only considers tokens th - `"take identifier"` - `"chuck identifier"` -This scope type is useful with ordinals, allow +This scope type is useful with ordinals, allowing you to say eg `"last identifier"` to refer to the last identifier on the current line. ##### `"paint"` From fe57dd4e761906da2d93ecfa773a98f9cd401db0 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 21 Oct 2022 16:18:32 +0100 Subject: [PATCH 64/69] Add jsdoc --- src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts b/src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts index 3b3b2960df..2c1b8944fc 100644 --- a/src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts +++ b/src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts @@ -3,6 +3,12 @@ import { matchText } from "../../../util/regex"; const CAMEL_REGEX = /\p{Lu}?\p{Ll}+|\p{Lu}+(?!\p{Ll})|\p{N}+/gu; +/** + * This class just encapsulates the word-splitting logic from + * {@link WordScopeHandler}. We could probably just inline it into that class, + * but for now we need it here because we can't yet properly mock away vscode + * for the unit tests in subtoken.test.ts. + */ export default class WordTokenizer { private wordRegex: RegExp; From 11ae8a0fbd79a07bffe4de34c343ab0e18887c0b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 21 Oct 2022 16:22:40 +0100 Subject: [PATCH 65/69] jsdoc --- .../modifiers/scopeHandlers/scopeHandler.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 2249032bb2..7b95bae577 100644 --- a/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -152,7 +152,7 @@ export interface ScopeHandler { * rightmost scope whose {@link Scope.domain.end} is equal or before * {@link position}. If {@link direction} is `"backward"` and {@link offset} * is 2, return the rightmost scope whose {@link Scope.domain.end} is equal - * or after the {@link Scope.domain.start} of the scope at `offset` 1. Etc. + * or before the {@link Scope.domain.start} of the scope at `offset` 1. Etc. * * Note that {@link offset} will always be greater than or equal to 1. * From 26438eaf4f8c151bad126279a4dec35234474455 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 21 Oct 2022 17:53:57 +0100 Subject: [PATCH 66/69] JSDocs --- .../modifiers/RelativeExclusiveScopeStage.ts | 30 ++++++++++++ .../modifiers/RelativeInclusiveScopeStage.ts | 49 ++++++++++++++----- .../modifiers/RelativeScopeStage.ts | 7 +++ 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index 46e0248953..eaaf0371ef 100644 --- a/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -9,6 +9,24 @@ import { runLegacy } from "./relativeScopeLegacy"; import getScopeHandler from "./scopeHandlers/getScopeHandler"; import type { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; +/** + * Handles relative modifiers that don't include targets intersecting with the + * input, eg "next funk", "previous two tokens". Proceeds as follows: + * + * 1. If the input is empty, skips past any scopes that are directly adjacent to + * input target in the direction of movement. Eg if the cursor is at the + * very start of a token, we first jump past that token for "next token". + * 2. Otherwise, we start at the `end` of the input range (`start` if + * {@link RelativeScopeModifier.direction} is `"backward"`). + * 3. Asks the scope handler for the scope at + * {@link RelativeScopeModifier.offset} in given + * {@link RelativeScopeModifier.direction} by calling + * {@link ScopeHandler.getScopeRelativeToPosition}. + * 4. If {@link RelativeScopeModifier.length} is 1, returns that scope + * 5. Otherwise, asks scope handler for scope at offset + * {@link RelativeScopeModifier.length} - 1, starting from the end of + * {@link Scope.domain} of that scope, and forms a range target. + */ export default class RelativeExclusiveScopeStage implements ModifierStage { constructor(private modifier: RelativeScopeModifier) {} @@ -60,6 +78,18 @@ export default class RelativeExclusiveScopeStage implements ModifierStage { } } +/** + * Determines the position to pass in to + * {@link ScopeHandler.getScopeRelativeToPosition}. If input target is empty, + * we skip past one scope if it is direclty adjacent to us in the direction + * we're going. Otherwise we just use end or start of input target, + * depending which direction we're going (`end` for `"forward"`). + * @param scopeHandler The scope handler to ask + * @param direction The direction we are going + * @param editor The editor containing {@link inputPosition} + * @param inputPosition The position of the input target + * @returns + */ function getInitialPositionForEmptyInputRange( scopeHandler: ScopeHandler, direction: string, diff --git a/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts index 020f7a6297..ca4e00114f 100644 --- a/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts +++ b/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts @@ -15,6 +15,29 @@ import type { TargetScope } from "./scopeHandlers/scope.types"; import type { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; import { TooFewScopesError } from "./TooFewScopesError"; +/** + * Handles relative modifiers that include targets intersecting with the input, + * eg `"two funks"`, `"token backward"`, etc. Proceeds as follows: + * + * 1. Gets all scopes intersecting with input target. For empty range, that + * will be the scope touching the input, preferring the one in the direction + * of {@link RelativeScopeModifier.direction} if the input is adjacent to + * two. For non-empty range, just queries + * {@link ScopeHandler.getScopesOverlappingRange}. These are called the + * offset 0 scopes, as they correspond to "offset 0". + * 2. Subtracts the number of scopes at offset 0 from + * {@link RelativeScopeModifier.length} to determine how many more are + * needed, throwing an error if offset zero already has more scopes than + * needed. + * 3. Calls {@link ScopeHandler.getScopeRelativeToPosition} starting from the + * end of the last offset 0 scope if direction is forward (start of the first + * if direction is backward). Uses `offset` determined from subtraction above + * to get enough scopes to result in {@link RelativeScopeModifier.length} + * total scopes. + * 4. Constructs a range target from the first offset 0 scope past the newly + * returned scope if direction is forward, or from last offset 0 scope if + * direction is backward. + */ export class RelativeInclusiveScopeStage implements ModifierStage { constructor(private modifier: RelativeScopeModifier) {} @@ -31,46 +54,46 @@ export class RelativeInclusiveScopeStage implements ModifierStage { const { isReversed, editor, contentRange: inputRange } = target; const { scopeType, length: desiredScopeCount, direction } = this.modifier; - const index0Scopes = getIndex0Scopes( + const offset0Scopes = getOffset0Scopes( scopeHandler, direction, editor, inputRange ); - const index0ScopeCount = index0Scopes.length; + const offset0ScopeCount = offset0Scopes.length; - if (index0ScopeCount === 0) { + if (offset0ScopeCount === 0) { throw new NoContainingScopeError(scopeType.type); } - if (index0ScopeCount > desiredScopeCount) { + if (offset0ScopeCount > desiredScopeCount) { throw new TooFewScopesError( desiredScopeCount, - index0ScopeCount, + offset0ScopeCount, scopeType.type ); } const proximalScope = - direction === "forward" ? index0Scopes[0] : index0Scopes.at(-1)!; + direction === "forward" ? offset0Scopes[0] : offset0Scopes.at(-1)!; const initialPosition = direction === "forward" - ? index0Scopes.at(-1)!.domain.end - : index0Scopes[0].domain.start; + ? offset0Scopes.at(-1)!.domain.end + : offset0Scopes[0].domain.start; const distalScope = - desiredScopeCount > index0ScopeCount + desiredScopeCount > offset0ScopeCount ? scopeHandler.getScopeRelativeToPosition( editor, initialPosition, - desiredScopeCount - index0ScopeCount, + desiredScopeCount - offset0ScopeCount, direction ) : direction === "forward" - ? index0Scopes.at(-1)! - : index0Scopes[0]; + ? offset0Scopes.at(-1)! + : offset0Scopes[0]; return [constructScopeRangeTarget(isReversed, proximalScope, distalScope)]; } @@ -87,7 +110,7 @@ export class RelativeInclusiveScopeStage implements ModifierStage { * @param range The input target range * @returns The scopes that are considered to be at index 0, ie "containing" / "intersecting" with the input target */ -function getIndex0Scopes( +function getOffset0Scopes( scopeHandler: ScopeHandler, direction: Direction, editor: TextEditor, diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts index 005da2c7ce..6165861506 100644 --- a/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -5,6 +5,13 @@ import type { ModifierStage } from "../PipelineStages.types"; import RelativeExclusiveScopeStage from "./RelativeExclusiveScopeStage"; import { RelativeInclusiveScopeStage } from "./RelativeInclusiveScopeStage"; +/** + * Implements relative scope modifiers like "next funk", "two tokens", etc. + * Proceeds by determining whether the modifier wants to include the scope(s) + * touching the input target (ie if {@link modifier.offset} is 0), and then + * delegating to {@link RelativeInclusiveScopeStage} if so, or + * {@link RelativeExclusiveScopeStage} if not. + */ export default class RelativeScopeStage implements ModifierStage { private modiferStage: ModifierStage; constructor(private modifier: RelativeScopeModifier) { From b4a0967a6f7632128777cbe65536396274ebf043 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 21 Oct 2022 17:55:22 +0100 Subject: [PATCH 67/69] doc tweaks --- src/processTargets/modifiers/RelativeInclusiveScopeStage.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts index ca4e00114f..742619ee52 100644 --- a/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts +++ b/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts @@ -100,7 +100,7 @@ export class RelativeInclusiveScopeStage implements ModifierStage { } /** - * Returns a list of scopes that are considered to be at relative scope index + * Returns a list of scopes that are considered to be at relative scope offset * 0, ie "containing" / "intersecting" with the input target. If the input * target is zero length, we return at most one scope, breaking ties by moving * in {@link direction} if the input position is adjacent to two scopes. @@ -108,7 +108,8 @@ export class RelativeInclusiveScopeStage implements ModifierStage { * @param direction The direction defined by the modifier * @param editor The editor containing {@link range} * @param range The input target range - * @returns The scopes that are considered to be at index 0, ie "containing" / "intersecting" with the input target + * @returns The scopes that are considered to be at offset 0, ie "containing" / + * "intersecting" with the input target */ function getOffset0Scopes( scopeHandler: ScopeHandler, From 6adadd7e2e0a1636526c0da09754a12cea0e6147 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 21 Oct 2022 17:55:52 +0100 Subject: [PATCH 68/69] reflow --- src/processTargets/modifiers/RelativeExclusiveScopeStage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index eaaf0371ef..c08412d08c 100644 --- a/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -82,8 +82,8 @@ export default class RelativeExclusiveScopeStage implements ModifierStage { * Determines the position to pass in to * {@link ScopeHandler.getScopeRelativeToPosition}. If input target is empty, * we skip past one scope if it is direclty adjacent to us in the direction - * we're going. Otherwise we just use end or start of input target, - * depending which direction we're going (`end` for `"forward"`). + * we're going. Otherwise we just use end or start of input target, depending + * which direction we're going (`end` for `"forward"`). * @param scopeHandler The scope handler to ask * @param direction The direction we are going * @param editor The editor containing {@link inputPosition} From eb271cd798889c4320130a3c4d1d0153ce7ab22b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 21 Oct 2022 18:01:25 +0100 Subject: [PATCH 69/69] Tweaks --- src/processTargets/modifiers/RelativeExclusiveScopeStage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index c08412d08c..4ee6ec0dc1 100644 --- a/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -25,7 +25,8 @@ import type { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; * 4. If {@link RelativeScopeModifier.length} is 1, returns that scope * 5. Otherwise, asks scope handler for scope at offset * {@link RelativeScopeModifier.length} - 1, starting from the end of - * {@link Scope.domain} of that scope, and forms a range target. + * {@link Scope.domain} of that scope (start for "backward"), and forms a + * range target. */ export default class RelativeExclusiveScopeStage implements ModifierStage { constructor(private modifier: RelativeScopeModifier) {}