api: no longer use namespaces

Namespaces are no longer being used, so APIs such as
`replace.byIndex` are now `replaceByIndex`. This slightly improves
the generated code and the editing experience.
This commit is contained in:
Grégoire Geis 2022-01-05 18:49:49 +01:00
parent 6603df95ea
commit ffe1db9f31
38 changed files with 3508 additions and 3221 deletions

59
meta.ts
View File

@ -13,16 +13,14 @@ const moduleCommentRe =
"m");
const docCommentRe =
new RegExp(String.raw`^( *)` // #1: indentation
+ String.raw`\/\*\*\n` // start of doc comment
+ String.raw`((?:\1 \*(?:\n| .+\n))+?)` // #2: doc comment
+ String.raw`\1 \*\/\n` // end of doc comment
+ String.raw`\1export (?:async )?function (\w+)` // #3: function name
+ String.raw`\((.*|\n[\s\S]+?^\1)\)` // #4: parameters
+ String.raw`(?:: )?(.+)[;{]$` // #5: return type (optional)
+ "|" // or
+ String.raw`^ *export namespace (\w+) {\n` // #6: namespace (alternative)
+ String.raw`^( +)`, // #7: namespace indentation
new RegExp(String.raw`^( *)` // #1: indentation
+ String.raw`\/\*\*\n` // start of doc comment
+ String.raw`((?:\1 \*(?:\n| .+\n))+?)` // #2: doc comment
+ String.raw`\1 \*\/\n` // end of doc comment
+ String.raw`\1export (?:async )?function (\w+)` // #3: function name
+ String.raw`(?:<[^>)\n]+>)?` // generic arguments
+ String.raw`\((.*|\n[\s\S]+?^\1)\)` // #4: parameters
+ String.raw`(?:: )?(.+)[;{]$`, // #5: return type (optional)
"gm");
function countNewLines(text: string) {
@ -145,11 +143,8 @@ function parseDocComments(code: string, modulePath: string) {
console.log("Parsing doc comments in module", moduleName);
}
const modulePrefix = moduleName === "misc" ? "" : moduleName + ".";
const functions: Builder.ParsedFunction[] = [],
namespaces: string[] = [];
let previousIndentation = 0;
const modulePrefix = moduleName === "misc" ? "" : moduleName + ".",
functions: Builder.ParsedFunction[] = [];
for (let match = docCommentRe.exec(code); match !== null; match = docCommentRe.exec(code)) {
const indentationString = match[1],
@ -157,20 +152,10 @@ function parseDocComments(code: string, modulePath: string) {
functionName = match[3],
parametersString = match[4],
returnTypeString = match[5],
enteredNamespace = match[6],
enteredNamespaceIndentation = match[7],
startLine = countNewLines(code.slice(0, match.index)),
endLine = startLine + countNewLines(match[0]);
if (enteredNamespace !== undefined) {
namespaces.push(enteredNamespace);
previousIndentation = enteredNamespaceIndentation.length;
continue;
}
const indentation = indentationString.length,
namespace = namespaces.length === 0 ? undefined : namespaces.join("."),
returnType = returnTypeString.trim(),
parameters = parametersString
.split(/,(?![^:]+?[}>])/g)
@ -203,11 +188,6 @@ function parseDocComments(code: string, modulePath: string) {
.map((line) => line.slice(indentation).replace(/^ \* ?/g, ""))
.join("\n");
if (previousIndentation > indentation) {
namespaces.pop();
previousIndentation = indentation;
}
for (const parameter of parameters) {
if (parameter[0].endsWith("?")) {
// Optional parameters.
@ -230,16 +210,16 @@ function parseDocComments(code: string, modulePath: string) {
properties[k] = v?.replace(/\n {2}/g, " ").trim();
return "";
}),
summary = /((?:.+(?:\n|$))+)/.exec(doc)![0].trim().replace(/\.$/, ""),
summary = (/((?:.+(?:\n|$))+)/.exec(doc) === null ? console.log(functionName, JSON.stringify(doc)) : 0, /((?:.+(?:\n|$))+)/.exec(doc)![0].trim().replace(/\.$/, "")),
examplesStrings = splitDocComment.slice(1),
nameWithDot = functionName.replace(/_/g, ".");
let qualifiedName = modulePrefix;
if (namespace !== undefined) {
qualifiedName += namespace + ".";
if ("internal" in properties) {
continue;
}
let qualifiedName = modulePrefix;
if (nameWithDot === moduleName) {
qualifiedName = qualifiedName.replace(/\.$/, "");
} else {
@ -247,7 +227,6 @@ function parseDocComments(code: string, modulePath: string) {
}
functions.push({
namespace,
name: functionName,
nameWithDot,
qualifiedName,
@ -370,9 +349,8 @@ export class Builder {
}
}
export namespace Builder {
export declare namespace Builder {
export interface ParsedFunction {
readonly namespace?: string;
readonly name: string;
readonly nameWithDot: string;
readonly qualifiedName: string;
@ -668,6 +646,11 @@ async function main() {
console.error("File", fileToCheck, "includes forbidden access to editor.selections.");
success = false;
}
if (/^(export )?namespace/m.test(contentToCheck)) {
console.error("File", fileToCheck, "includes a non-`declare` namespace.");
success = false;
}
}
}

View File

@ -222,6 +222,12 @@ export class ContextWithoutActiveEditor {
* The context of execution of a script.
*/
export class Context extends ContextWithoutActiveEditor {
/**
* The base {@link Context} class, which does not require an active
* {@link vscode.TextEditor}.
*/
public static readonly WithoutActiveEditor = ContextWithoutActiveEditor;
/**
* Returns the current execution context, or throws an error if called outside
* of an execution context or if the execution context does not have an
@ -254,6 +260,31 @@ export class Context extends ContextWithoutActiveEditor {
}
}
/**
* Returns a {@link Context} or {@link Context.WithoutActiveEditor} depending
* on whether there is an active text editor.
*/
public static create(extension: Extension, command: CommandDescriptor) {
const activeEditorState = extension.editors.active,
cancellationToken = extension.cancellationToken;
return activeEditorState === undefined
? new Context.WithoutActiveEditor(extension, cancellationToken, command)
: new Context(activeEditorState, cancellationToken, command);
}
/**
* Returns a {@link Context} or throws an exception if there is no active text
* editor.
*/
public static createWithActiveTextEditor(extension: Extension, command: CommandDescriptor) {
const activeEditorState = extension.editors.active;
EditorRequiredError.throwUnlessAvailable(activeEditorState);
return new Context(activeEditorState, extension.cancellationToken, command);
}
private _document: vscode.TextDocument;
private _editor: vscode.TextEditor;
private _mode: Mode;
@ -457,39 +488,8 @@ export class Context extends ContextWithoutActiveEditor {
}
}
export namespace Context {
/**
* The base `Context` class, which does not require an active
* `vscode.TextEditor`.
*/
export const WithoutActiveEditor = ContextWithoutActiveEditor;
export declare namespace Context {
export type WithoutActiveEditor = ContextWithoutActiveEditor;
/**
* Returns a `Context` or `Context.WithoutActiveEditor` depending on whether
* there is an active text editor.
*/
export function create(extension: Extension, command: CommandDescriptor) {
const activeEditorState = extension.editors.active,
cancellationToken = extension.cancellationToken;
return activeEditorState === undefined
? new Context.WithoutActiveEditor(extension, cancellationToken, command)
: new Context(activeEditorState, cancellationToken, command);
}
/**
* Returns a `Context` or throws an exception if there is no active text
* editor.
*/
export function createWithActiveTextEditor(extension: Extension, command: CommandDescriptor) {
const activeEditorState = extension.editors.active;
EditorRequiredError.throwUnlessAvailable(activeEditorState);
return new Context(activeEditorState, extension.cancellationToken, command);
}
}
/**

View File

@ -3,7 +3,7 @@ import * as vscode from "vscode";
import { Context, edit } from "../context";
import * as Positions from "../positions";
import * as Selections from "../selections";
import { TrackedSelection } from "../../utils/tracked-selection";
import * as TrackedSelection from "../../utils/tracked-selection";
const enum Constants {
PositionMask = 0b00_11_1,
@ -166,16 +166,16 @@ export function insert(
f: insert.Callback<insert.Result> | insert.Callback<insert.AsyncResult>,
selections?: readonly vscode.Selection[],
): Thenable<vscode.Selection[]> {
return insert.byIndex(
return insertByIndex(
flags,
(i, selection, document) => f(document.getText(selection), selection, i, document) as any,
selections,
);
}
export namespace insert {
export declare namespace insert {
/**
* Insertion flags for `insert`.
* Insertion flags for {@link insert}.
*/
export const enum Flags {
/**
@ -219,358 +219,373 @@ export namespace insert {
Extend = 0b10_00_1,
}
export const Replace = Flags.Replace,
Start = Flags.Start,
End = Flags.End,
Active = Flags.Active,
Anchor = Flags.Anchor,
Keep = Flags.Keep,
Select = Flags.Select,
Extend = Flags.Extend;
export function flagsAtEdge(edge?: "active" | "anchor" | "start" | "end") {
switch (edge) {
case undefined:
return Flags.Replace;
case "active":
return Flags.Active;
case "anchor":
return Flags.Anchor;
case "start":
return Flags.Start;
case "end":
return Flags.End;
}
}
/**
* The result of a callback passed to `insert` or `insert.byIndex`.
* The result of a callback passed to {@link insert} or
* {@link insertByIndex}.
*/
export type Result = string | undefined;
/**
* The result of an async callback passed to `insert` or `insert.byIndex`.
* The result of an async callback passed to {@link insert} or
* {@link insertByIndex}.
*/
export type AsyncResult = Thenable<Result>;
/**
* A callback passed to `insert`.
* A callback passed to {@link insert}.
*/
export interface Callback<T> {
(text: string, selection: vscode.Selection, index: number, document: vscode.TextDocument): T;
}
/**
* A callback passed to `insert.byIndex`.
*/
export interface ByIndexCallback<T> {
(index: number, selection: vscode.Selection, document: vscode.TextDocument): T;
export const Replace: Flags.Replace,
Start: Flags.Start,
End: Flags.End,
Active: Flags.Active,
Anchor: Flags.Anchor,
Keep: Flags.Keep,
Select: Flags.Select,
Extend: Flags.Extend;
}
for (const [k, v] of Object.entries({
Replace: insert.Flags.Replace,
Start: insert.Flags.Start,
End: insert.Flags.End,
Active: insert.Flags.Active,
Anchor: insert.Flags.Anchor,
Keep: insert.Flags.Keep,
Select: insert.Flags.Select,
Extend: insert.Flags.Extend,
})) {
Object.defineProperty(insert, k, { value: v });
}
export function insertFlagsAtEdge(edge?: "active" | "anchor" | "start" | "end") {
switch (edge) {
case undefined:
return insert.Flags.Replace;
case "active":
return insert.Flags.Active;
case "anchor":
return insert.Flags.Anchor;
case "start":
return insert.Flags.Start;
case "end":
return insert.Flags.End;
}
}
/**
* Inserts text next to the given selections according to the given function.
*
* @param f A mapping function called for each selection; given the index,
* range and editor of each selection, it should return the new text content
* of the selection, or `undefined` if it is to be removed. Also works for
* `async` (i.e. `Promise`-returning) functions, in which case **all**
* results must be promises.
* @param selections If `undefined`, the selections of the active editor will
* be used. Otherwise, must be a `vscode.Selection` array which will be
* mapped in the active editor.
*
* ### Example
* ```js
* Selections.set(await insertByIndex(insert.Start, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* 1a 2b 3c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* ### Example
* ```js
* Selections.set(await insertByIndex(insert.Start | insert.Select, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* 1a 2b 3c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* ### Example
* ```js
* Selections.set(await insertByIndex(insert.Start | insert.Extend, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* 1a 2b 3c
* ^^ 0
* ^^ 1
* ^^ 2
* ```
*
* ### Example
* ```js
* Selections.set(await insertByIndex(insert.End, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* a1 b2 c3
* ^ 0
* ^ 1
* ^ 2
* ```
*
* ### Example
* ```js
* Selections.set(await insertByIndex(insert.End | insert.Select, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* a1 b2 c3
* ^ 0
* ^ 1
* ^ 2
* ```
*
* ### Example
* ```js
* Selections.set(await insertByIndex(insert.End | insert.Extend, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* a1 b2 c3
* ^^ 0
* ^^ 1
* ^^ 2
* ```
*/
export function insertByIndex(
flags: insert.Flags,
f: insertByIndex.Callback<insert.Result> | insertByIndex.Callback<insert.AsyncResult>,
selections: readonly vscode.Selection[] = Context.current.selections,
): Thenable<vscode.Selection[]> {
if (selections.length === 0) {
return Context.wrap(Promise.resolve([]));
}
/**
* Inserts text next to the given selections according to the given function.
*
* @param f A mapping function called for each selection; given the index,
* range and editor of each selection, it should return the new text content
* of the selection, or `undefined` if it is to be removed. Also works for
* `async` (i.e. `Promise`-returning) functions, in which case **all**
* results must be promises.
* @param selections If `undefined`, the selections of the active editor will
* be used. Otherwise, must be a `vscode.Selection` array which will be
* mapped in the active editor.
*
* ### Example
* ```js
* Selections.set(await insert.byIndex(insert.Start, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* 1a 2b 3c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* ### Example
* ```js
* Selections.set(await insert.byIndex(insert.Start | insert.Select, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* 1a 2b 3c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* ### Example
* ```js
* Selections.set(await insert.byIndex(insert.Start | insert.Extend, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* 1a 2b 3c
* ^^ 0
* ^^ 1
* ^^ 2
* ```
*
* ### Example
* ```js
* Selections.set(await insert.byIndex(insert.End, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* a1 b2 c3
* ^ 0
* ^ 1
* ^ 2
* ```
*
* ### Example
* ```js
* Selections.set(await insert.byIndex(insert.End | insert.Select, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* a1 b2 c3
* ^ 0
* ^ 1
* ^ 2
* ```
*
* ### Example
* ```js
* Selections.set(await insert.byIndex(insert.End | insert.Extend, (i) => `${i + 1}`));
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* a1 b2 c3
* ^^ 0
* ^^ 1
* ^^ 2
* ```
*/
export function byIndex(
flags: Flags,
f: ByIndexCallback<Result> | ByIndexCallback<AsyncResult>,
selections: readonly vscode.Selection[] = Context.current.selections,
): Thenable<vscode.Selection[]> {
if (selections.length === 0) {
return Context.wrap(Promise.resolve([]));
}
const document = Context.current.document,
firstResult = f(0, selections[0], document);
const document = Context.current.document,
firstResult = f(0, selections[0], document);
if (typeof firstResult === "object") {
// `f` returns promises.
const promises = [firstResult];
for (let i = 1, len = selections.length; i < len; i++) {
promises.push(f(i, selections[i], document) as AsyncResult);
}
return Context.wrap(
Promise
.all(promises)
.then((results) => mapResults(flags, document, selections, results)),
);
}
// `f` returns regular values.
const allResults: Result[] = [firstResult];
if (typeof firstResult === "object") {
// `f` returns promises.
const promises = [firstResult];
for (let i = 1, len = selections.length; i < len; i++) {
allResults.push(f(i, selections[i], document) as Result);
promises.push(f(i, selections[i], document) as insert.AsyncResult);
}
return mapResults(flags, document, selections, allResults);
return Context.wrap(
Promise
.all(promises)
.then((results) => mapResults(flags, document, selections, results)),
);
}
export namespace byIndex {
/**
* Same as `insert.byIndex`, but also inserts strings that end with a
* newline character on the next or previous line.
*/
export async function withFullLines(
flags: Flags,
f: ByIndexCallback<Result> | ByIndexCallback<AsyncResult>,
selections: readonly vscode.Selection[] = Context.current.selections,
) {
const document = Context.current.document,
allResults = await Promise.all(selections.map((sel, i) => f(i, sel, document)));
// `f` returns regular values.
const allResults: insert.Result[] = [firstResult];
// Separate full-line results from all results.
const results: Result[] = [],
resultsSelections: vscode.Selection[] = [],
fullLineResults: Result[] = [],
fullLineResultsSelections: vscode.Selection[] = [],
isFullLines: boolean[] = [];
for (let i = 1, len = selections.length; i < len; i++) {
allResults.push(f(i, selections[i], document) as insert.Result);
}
for (let i = 0; i < allResults.length; i++) {
const result = allResults[i];
return mapResults(flags, document, selections, allResults);
}
if (result === undefined) {
continue;
}
export declare namespace insertByIndex {
/**
* A callback passed to {@link insertByIndex}.
*/
export interface Callback<T> {
(index: number, selection: vscode.Selection, document: vscode.TextDocument): T;
}
}
if (result.endsWith("\n")) {
fullLineResults.push(result);
fullLineResultsSelections.push(selections[i]);
isFullLines.push(true);
} else {
results.push(result);
resultsSelections.push(selections[i]);
isFullLines.push(false);
}
}
/**
* Same as {@link insertByIndex}, but also inserts strings that end with a
* newline character on the next or previous line.
*/
export async function insertByIndexWithFullLines(
flags: insert.Flags,
f: insertByIndex.Callback<insert.Result> | insertByIndex.Callback<insert.AsyncResult>,
selections: readonly vscode.Selection[] = Context.current.selections,
) {
const document = Context.current.document,
allResults = await Promise.all(selections.map((sel, i) => f(i, sel, document)));
if (fullLineResults.length === 0) {
return await mapResults(flags, document, resultsSelections, results);
}
// Separate full-line results from all results.
const results: insert.Result[] = [],
resultsSelections: vscode.Selection[] = [],
fullLineResults: insert.Result[] = [],
fullLineResultsSelections: vscode.Selection[] = [],
isFullLines: boolean[] = [];
let savedSelections = new TrackedSelection.Set(
TrackedSelection.fromArray(fullLineResultsSelections, document),
document,
);
for (let i = 0; i < allResults.length; i++) {
const result = allResults[i];
// Insert non-full lines.
const normalSelections = await mapResults(flags, document, resultsSelections, results);
if (result === undefined) {
continue;
}
// Insert full lines.
const fullLineSelections = savedSelections.restore();
if (result.endsWith("\n")) {
fullLineResults.push(result);
fullLineResultsSelections.push(selections[i]);
isFullLines.push(true);
} else {
results.push(result);
resultsSelections.push(selections[i]);
isFullLines.push(false);
}
}
savedSelections.dispose();
if (fullLineResults.length === 0) {
return await mapResults(flags, document, resultsSelections, results);
}
const nextFullLineSelections: vscode.Selection[] = [],
insertionPositions: vscode.Position[] = [];
let savedSelections = new TrackedSelection.Set(
TrackedSelection.fromArray(fullLineResultsSelections, document),
document,
);
if ((flags & Constants.PositionMask) === Flags.Start) {
for (const selection of fullLineSelections) {
const insertionPosition = Positions.lineStart(selection.start.line);
// Insert non-full lines.
const normalSelections = await mapResults(flags, document, resultsSelections, results);
insertionPositions.push(insertionPosition);
// Insert full lines.
const fullLineSelections = savedSelections.restore();
if ((flags & Constants.BehaviorMask) === Flags.Extend) {
nextFullLineSelections.push(
Selections.fromStartEnd(
insertionPosition, selection.end, selection.isReversed, document),
);
} else if ((flags & Constants.BehaviorMask) === Flags.Select) {
nextFullLineSelections.push(Selections.empty(insertionPosition));
} else {
// Keep selection as is.
nextFullLineSelections.push(selection);
}
}
savedSelections.dispose();
const nextFullLineSelections: vscode.Selection[] = [],
insertionPositions: vscode.Position[] = [];
if ((flags & Constants.PositionMask) === insert.Flags.Start) {
for (const selection of fullLineSelections) {
const insertionPosition = Positions.lineStart(selection.start.line);
insertionPositions.push(insertionPosition);
if ((flags & Constants.BehaviorMask) === insert.Flags.Extend) {
nextFullLineSelections.push(
Selections.fromStartEnd(
insertionPosition, selection.end, selection.isReversed, document),
);
} else if ((flags & Constants.BehaviorMask) === insert.Flags.Select) {
nextFullLineSelections.push(Selections.empty(insertionPosition));
} else {
for (const selection of fullLineSelections) {
const insertionPosition = Positions.lineStart(Selections.endLine(selection) + 1);
insertionPositions.push(insertionPosition);
if ((flags & Constants.BehaviorMask) === Flags.Extend) {
nextFullLineSelections.push(
Selections.fromStartEnd(
selection.start, insertionPosition, selection.isReversed, document),
);
} else if ((flags & Constants.BehaviorMask) === Flags.Select) {
nextFullLineSelections.push(Selections.empty(insertionPosition));
} else {
// Keep selection as is.
nextFullLineSelections.push(selection);
}
}
// Keep selection as is.
nextFullLineSelections.push(selection);
}
}
} else {
for (const selection of fullLineSelections) {
const insertionPosition = Positions.lineStart(Selections.endLine(selection) + 1);
savedSelections = new TrackedSelection.Set(
TrackedSelection.fromArray(nextFullLineSelections, document),
document,
(flags & Constants.BehaviorMask) === Flags.Keep
? TrackedSelection.Flags.Strict
: TrackedSelection.Flags.Inclusive,
);
insertionPositions.push(insertionPosition);
await edit((editBuilder) => {
for (let i = 0; i < insertionPositions.length; i++) {
editBuilder.replace(insertionPositions[i], fullLineResults[i]!);
}
});
const finalFullLineSelections = savedSelections.restore();
savedSelections.dispose();
// Merge back selections.
const allSelections: vscode.Selection[] = [];
for (let i = 0, normalIdx = 0, fullLineIdx = 0; i < isFullLines.length; i++) {
if (isFullLines[i]) {
allSelections.push(finalFullLineSelections[fullLineIdx++]);
} else {
allSelections.push(normalSelections[normalIdx++]);
}
if ((flags & Constants.BehaviorMask) === insert.Flags.Extend) {
nextFullLineSelections.push(
Selections.fromStartEnd(
selection.start, insertionPosition, selection.isReversed, document),
);
} else if ((flags & Constants.BehaviorMask) === insert.Flags.Select) {
nextFullLineSelections.push(Selections.empty(insertionPosition));
} else {
// Keep selection as is.
nextFullLineSelections.push(selection);
}
return allSelections;
}
}
savedSelections = new TrackedSelection.Set(
TrackedSelection.fromArray(nextFullLineSelections, document),
document,
(flags & Constants.BehaviorMask) === insert.Flags.Keep
? TrackedSelection.Flags.Strict
: TrackedSelection.Flags.Inclusive,
);
await edit((editBuilder) => {
for (let i = 0; i < insertionPositions.length; i++) {
editBuilder.replace(insertionPositions[i], fullLineResults[i]!);
}
});
const finalFullLineSelections = savedSelections.restore();
savedSelections.dispose();
// Merge back selections.
const allSelections: vscode.Selection[] = [];
for (let i = 0, normalIdx = 0, fullLineIdx = 0; i < isFullLines.length; i++) {
if (isFullLines[i]) {
allSelections.push(finalFullLineSelections[fullLineIdx++]);
} else {
allSelections.push(normalSelections[normalIdx++]);
}
}
return allSelections;
}
/**
@ -613,70 +628,74 @@ export function replace(
return insert(insert.Flags.Replace, f, selections);
}
export namespace replace {
export declare namespace replace {
/**
* The result of a callback passed to `replace` or `replace.byIndex`.
* The result of a callback passed to {@link replace} or
* {@link replaceByIndex}.
*/
export type Result = string | undefined;
/**
* The result of an async callback passed to `replace` or `replace.byIndex`.
* The result of an async callback passed to {@link replace} or
* {@link replaceByIndex}.
*/
export type AsyncResult = Thenable<Result>;
/**
* A callback passed to `replace`.
* A callback passed to {@link replace}.
*/
export interface Callback<T> {
(text: string, selection: vscode.Selection, index: number, document: vscode.TextDocument): T;
}
}
/**
* Replaces the given selections according to the given function.
*
* @param f A mapping function called for each selection; given the index,
* range and editor of each selection, it should return the new text content
* of the selection, or `undefined` if it is to be removed. Also works for
* `async` (i.e. `Promise`-returning) functions, in which case **all**
* results must be promises.
* @param selections If `undefined`, the selections of the active editor will
* be used. Otherwise, must be a `vscode.Selection` array which will be
* mapped in the active editor.
*
* ### Example
* ```js
* await replaceByIndex((i) => `${i + 1}`);
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* 1 2 3
* ^ 0
* ^ 1
* ^ 2
* ```
*/
export function replaceByIndex(
f: replaceByIndex.Callback<replace.Result> | replaceByIndex.Callback<replace.AsyncResult>,
selections?: readonly vscode.Selection[],
): Thenable<vscode.Selection[]> {
return insertByIndex(insert.Flags.Replace, f, selections);
}
export declare namespace replaceByIndex {
/**
* A callback passed to `replace.byIndex`.
* A callback passed to {@link replaceByIndex}.
*/
export interface ByIndexCallback<T> {
export interface Callback<T> {
(index: number, selection: vscode.Selection, document: vscode.TextDocument): T;
}
/**
* Replaces the given selections according to the given function.
*
* @param f A mapping function called for each selection; given the index,
* range and editor of each selection, it should return the new text content
* of the selection, or `undefined` if it is to be removed. Also works for
* `async` (i.e. `Promise`-returning) functions, in which case **all**
* results must be promises.
* @param selections If `undefined`, the selections of the active editor will
* be used. Otherwise, must be a `vscode.Selection` array which will be
* mapped in the active editor.
*
* ### Example
* ```js
* await replace.byIndex((i) => `${i + 1}`);
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* 1 2 3
* ^ 0
* ^ 1
* ^ 2
* ```
*/
export function byIndex(
f: ByIndexCallback<Result> | ByIndexCallback<AsyncResult>,
selections?: readonly vscode.Selection[],
): Thenable<vscode.Selection[]> {
return insert.byIndex(insert.Flags.Replace, f, selections);
}
}
/**
@ -708,84 +727,80 @@ export namespace replace {
* ```
*/
export function rotate(by: number, selections?: readonly vscode.Selection[]) {
return rotate
.contentsOnly(by, selections)
.then((selections) => rotate.selectionsOnly(by, selections));
return rotateContents(by, selections).then((selections) => rotateSelections(by, selections));
}
export namespace rotate {
/**
* Rotates the contents of the given selections by the given offset.
*
* @see rotate
*
* ### Example
* ```js
* await rotate.contentsOnly(1);
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* b c a
* ^ 0
* ^ 1
* ^ 2
* ```
*/
export function contentsOnly(
by: number,
selections: readonly vscode.Selection[] = Context.current.selections,
) {
const len = selections.length;
/**
* Rotates the contents of the given selections by the given offset.
*
* @see {@link rotate}
*
* ### Example
* ```js
* await rotateContents(1);
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* b c a
* ^ 0
* ^ 1
* ^ 2
* ```
*/
export function rotateContents(
by: number,
selections: readonly vscode.Selection[] = Context.current.selections,
) {
const len = selections.length;
// Handle negative values for `by`:
by = (by % len) + len;
// Handle negative values for `by`:
by = (by % len) + len;
if (by === len) {
return Context.wrap(Promise.resolve(selections.slice()));
}
return replace.byIndex(
(i, _, document) => document.getText(selections[(i + by) % len]),
selections,
);
if (by === len) {
return Context.wrap(Promise.resolve(selections.slice()));
}
/**
* Rotates the given selections (but not their contents) by the given offset.
*
* @see rotate
*
* ### Example
* ```js
* rotate.selectionsOnly(1);
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* a b c
* ^ 1
* ^ 2
* ^ 0
* ```
*/
export function selectionsOnly(by: number, selections?: readonly vscode.Selection[]) {
Selections.set(Selections.rotate(by, selections));
}
return replaceByIndex(
(i, _, document) => document.getText(selections[(i + by) % len]),
selections,
);
}
/**
* Rotates the given selections (but not their contents) by the given offset.
*
* @see {@link rotate}
*
* ### Example
* ```js
* rotateSelections(1);
* ```
*
* Before:
* ```
* a b c
* ^ 0
* ^ 1
* ^ 2
* ```
*
* After:
* ```
* a b c
* ^ 1
* ^ 2
* ^ 0
* ```
*/
export function rotateSelections(by: number, selections?: readonly vscode.Selection[]) {
Selections.set(Selections.rotate(by, selections));
}

View File

@ -17,7 +17,6 @@ export * from "./search/lines";
export * from "./search/move";
export * from "./search/move-to";
export * from "./search/pairs";
export * from "./search/range";
export * from "./search/word";
export * from "./types";
@ -27,6 +26,11 @@ export * from "./types";
export * as Lines from "./lines";
export { firstVisibleLine, middleVisibleLine, lastVisibleLine } from "./lines";
/**
* Operations on ranges of text objects.
*/
export * as Range from "./search/range";
/**
* Operations on `vscode.Position`s.
*/

View File

@ -2,6 +2,6 @@
/**
* API for generating VS Code-compatible keybindings.
*/
export namespace Keybindings {
export declare namespace Keybindings {
}

View File

@ -148,61 +148,57 @@ export function column(
return new vscode.Position(line.line, text.length + diffAddedByTabs(text, editor));
}
export namespace column {
/**
* Returns the `vscode.Position`-compatible position for the given position.
* Reverses the diff added by `column`.
*/
export function character(
position: vscode.Position,
editor?: Pick<vscode.TextEditor, "document" | "options">,
roundUp?: boolean,
): vscode.Position;
/**
* Returns the {@link vscode.Position}-compatible position for the given
* position. Reverses the diff added by {@link column}.
*/
export function character(
position: vscode.Position,
editor?: Pick<vscode.TextEditor, "document" | "options">,
roundUp?: boolean,
): vscode.Position;
/**
* Returns the `vscode.Position`-compatible character for the given column.
* Reverses the diff added by `column`.
*/
export function character(
line: number,
character: number,
editor?: Pick<vscode.TextEditor, "document" | "options">,
roundUp?: boolean,
): number;
/**
* Returns the {@link vscode.Position}-compatible character for the given
* column. Reverses the diff added by {@link column}.
*/
export function character(
line: number,
character: number,
editor?: Pick<vscode.TextEditor, "document" | "options">,
roundUp?: boolean,
): number;
export function character(
lineOrPosition: number | vscode.Position,
characterOrEditor?: number | Pick<vscode.TextEditor, "document" | "options">,
editorOrRoundUp?: Pick<vscode.TextEditor, "document" | "options"> | boolean,
roundUp?: boolean,
) {
if (typeof lineOrPosition === "number") {
// Second overload.
const line = lineOrPosition,
character = characterOrEditor as number,
editor = editorOrRoundUp as vscode.TextEditor ?? Context.current.editor;
export function character(
lineOrPosition: number | vscode.Position,
characterOrEditor?: number | Pick<vscode.TextEditor, "document" | "options">,
editorOrRoundUp?: Pick<vscode.TextEditor, "document" | "options"> | boolean,
roundUp?: boolean,
) {
if (typeof lineOrPosition === "number") {
// Second overload.
const line = lineOrPosition,
character = characterOrEditor as number,
editor = editorOrRoundUp as vscode.TextEditor ?? Context.current.editor;
return getCharacter(editor.document.lineAt(line).text, character, editor, roundUp ?? false);
}
// First overload.
const position = lineOrPosition,
editor = characterOrEditor as vscode.TextEditor ?? Context.current.editor;
roundUp = editorOrRoundUp as boolean ?? false;
const text = editor.document.lineAt(position.line).text;
return new vscode.Position(
position.line, getCharacter(text, position.character, editor, roundUp));
return getCharacter(editor.document.lineAt(line).text, character, editor, roundUp ?? false);
}
// First overload.
const position = lineOrPosition,
editor = characterOrEditor as vscode.TextEditor ?? Context.current.editor;
roundUp = editorOrRoundUp as boolean ?? false;
const text = editor.document.lineAt(position.line).text;
return new vscode.Position(
position.line, getCharacter(text, position.character, editor, roundUp));
}
/**
* Same as `Lines.length`, but also increases the count according to tab
* Same as {@link length}, but also increases the count according to tab
* characters so that the result matches the rendered view.
*
* @see Lines.length
*/
export function columns(
line: number | vscode.Position,

View File

@ -1,13 +1,13 @@
import * as vscode from "vscode";
import { Context } from "./context";
import { keypress, prompt } from "./prompt";
import { keypress, promptLocked, promptOne } from "./prompt";
export interface Menu {
readonly items: Menu.Items;
}
export namespace Menu {
export declare namespace Menu {
export interface Items {
[keys: string]: Item;
}
@ -100,7 +100,7 @@ export async function showMenu(
) {
const entries = Object.entries(menu.items);
const items = entries.map((x) => [x[0], x[1].text] as const);
const choice = await prompt.one(items);
const choice = await promptOne(items);
if (typeof choice === "string") {
if (prefix !== undefined) {
@ -118,66 +118,64 @@ export async function showMenu(
);
}
export namespace showMenu {
/**
* Shows the menu with the given name.
*/
export function byName(
menuName: string,
additionalArgs: readonly any[] = [],
prefix?: string,
) {
return showMenu(findMenu(menuName), additionalArgs, prefix);
}
/**
* Shows the menu with the given name.
*/
export function showMenuByName(
menuName: string,
additionalArgs: readonly any[] = [],
prefix?: string,
) {
return showMenu(findMenu(menuName), additionalArgs, prefix);
}
/**
* Same as `showMenu`, but only displays the menu after a specified delay.
*/
export async function withDelay(
delayMs: number,
menu: Menu,
additionalArgs: readonly any[] = [],
prefix?: string,
) {
const cancellationTokenSource = new vscode.CancellationTokenSource(),
currentContext = Context.current;
/**
* Same as {@link showMenu}, but only displays the menu after a specified delay.
*/
export async function showMenuAfterDelay(
delayMs: number,
menu: Menu,
additionalArgs: readonly any[] = [],
prefix?: string,
) {
const cancellationTokenSource = new vscode.CancellationTokenSource(),
currentContext = Context.current;
currentContext.cancellationToken.onCancellationRequested(() =>
cancellationTokenSource.cancel());
currentContext.cancellationToken.onCancellationRequested(() =>
cancellationTokenSource.cancel());
const keypressContext = currentContext.withCancellationToken(cancellationTokenSource.token),
timeout = setTimeout(() => cancellationTokenSource.cancel(), delayMs);
const keypressContext = currentContext.withCancellationToken(cancellationTokenSource.token),
timeout = setTimeout(() => cancellationTokenSource.cancel(), delayMs);
try {
const key = await keypress(keypressContext);
try {
const key = await keypress(keypressContext);
clearTimeout(timeout);
clearTimeout(timeout);
for (const itemKeys in menu.items) {
if (!itemKeys.includes(key)) {
continue;
}
const pickedItem = menu.items[itemKeys],
args = mergeArgs(pickedItem.args, additionalArgs);
return Context.WithoutActiveEditor.wrap(
vscode.commands.executeCommand(pickedItem.command, ...args),
);
for (const itemKeys in menu.items) {
if (!itemKeys.includes(key)) {
continue;
}
if (prefix !== undefined) {
await vscode.commands.executeCommand("default:type", { text: prefix + key });
}
} catch (e) {
if (!currentContext.cancellationToken.isCancellationRequested) {
return showMenu(menu, additionalArgs, prefix);
}
const pickedItem = menu.items[itemKeys],
args = mergeArgs(pickedItem.args, additionalArgs);
throw e;
} finally {
cancellationTokenSource.dispose();
return Context.WithoutActiveEditor.wrap(
vscode.commands.executeCommand(pickedItem.command, ...args),
);
}
if (prefix !== undefined) {
await vscode.commands.executeCommand("default:type", { text: prefix + key });
}
} catch (e) {
if (!currentContext.cancellationToken.isCancellationRequested) {
return showMenu(menu, additionalArgs, prefix);
}
throw e;
} finally {
cancellationTokenSource.dispose();
}
}
@ -194,19 +192,17 @@ export async function showLockedMenu(
vscode.commands.executeCommand(
item.command, ...mergeArgs(item.args, additionalArgs))] as const);
await prompt.one.locked(items);
await promptLocked(items);
}
export namespace showLockedMenu {
/**
* Shows the menu with the given name.
*/
export function byName(
menuName: string,
additionalArgs: readonly any[] = [],
) {
return showLockedMenu(findMenu(menuName), additionalArgs);
}
/**
* Shows the menu with the given name.
*/
export function showLockedMenyByName(
menuName: string,
additionalArgs: readonly any[] = [],
) {
return showLockedMenu(findMenu(menuName), additionalArgs);
}
function mergeArgs(args: readonly any[] | undefined, additionalArgs: readonly any[]) {

View File

@ -80,23 +80,21 @@ export function offset(
return document.positionAt(document.offsetAt(position) + by);
}
export namespace offset {
/**
* Same as `offset`, but clamps to document edges.
*/
export function orEdge(
position: vscode.Position,
by: number,
document?: vscode.TextDocument,
) {
const result = offset(position, by, document);
/**
* Same as {@link offset}, but clamps to document edges.
*/
export function offsetOrEdge(
position: vscode.Position,
by: number,
document?: vscode.TextDocument,
) {
const result = offset(position, by, document);
if (result === undefined) {
return by < 0 ? zero : last(document);
}
return result;
if (result === undefined) {
return by < 0 ? zero : last(document);
}
return result;
}
/**
@ -150,8 +148,8 @@ export function lineBreak(line: number, document = Context.current.document) {
/**
* Returns the last position of the current document when going in the given
* direction. If `Backward`, this is `Positions.zero`. If `Forward`, this is
* `Positions.last(document)`.
* direction. If {@link Direction.Backward}, this is {@link zero}. If
* {@link Direction.Forward}, this is {@link last(document)}.
*/
export function edge(direction: Direction, document?: vscode.TextDocument) {
return direction === Direction.Backward ? zero : last(document);

View File

@ -5,7 +5,7 @@ import { set as setSelections } from "./selections";
import type { Input, SetInput } from "../commands";
import { ArgumentError, CancellationError } from "../utils/errors";
const actionEvent = new vscode.EventEmitter<Parameters<typeof prompt.notifyActionRequested>[0]>();
const actionEvent = new vscode.EventEmitter<Parameters<typeof notifyPromptActionRequested>[0]>();
/**
* Displays a prompt to the user.
@ -167,299 +167,302 @@ export function prompt(
return context.wrap(promise);
}
export namespace prompt {
type RegExpFlag = "m" | "u" | "s" | "y" | "i" | "g";
type RegExpFlags = RegExpFlag
| `${RegExpFlag}${RegExpFlag}`
| `${RegExpFlag}${RegExpFlag}${RegExpFlag}`
| `${RegExpFlag}${RegExpFlag}${RegExpFlag}${RegExpFlag}`;
export declare namespace prompt {
/**
* Options for spawning a `prompt`.
* Options for spawning a {@link prompt}.
*/
export interface Options extends vscode.InputBoxOptions {
readonly history?: string[];
readonly historySize?: number;
}
}
/**
* Returns `vscode.InputBoxOptions` that only validate if a number in a given
* range is entered.
*/
export function numberOpts(
opts: { integer?: boolean; range?: [number, number] } = {},
): vscode.InputBoxOptions {
return {
validateInput(input) {
const n = +input;
/**
* Returns {@link vscode.InputBoxOptions} that only validate if a number in a
* given range is entered.
*/
export function promptNumberOpts(
opts: { integer?: boolean; range?: [number, number] } = {},
): vscode.InputBoxOptions {
return {
validateInput(input) {
const n = +input;
if (isNaN(n)) {
return "Invalid number.";
}
if (opts.range && (n < opts.range[0] || n > opts.range[1])) {
return `Number out of range ${JSON.stringify(opts.range)}.`;
}
if (opts.integer && (n | 0) !== n) {
return `Number must be an integer.`;
}
return;
},
};
}
/**
* Equivalent to `+await prompt(numberOpts(), context)`.
*/
export function number(
opts: Parameters<typeof numberOpts>[0],
context = Context.WithoutActiveEditor.current,
) {
return prompt(numberOpts(opts), context).then((x) => +x);
}
/**
* Last used inputs for `regexp` prompts.
*/
export const regexpHistory: string[] = [];
/**
* Returns `vscode.InputBoxOptions` that only validate if a valid ECMAScript
* regular expression is entered.
*/
export function regexpOpts(flags: RegExpFlags): prompt.Options {
return {
prompt: "Regular expression",
validateInput(input) {
if (input.length === 0) {
return "RegExp cannot be empty";
}
try {
new RegExp(input, flags);
return undefined;
} catch {
return "invalid RegExp";
}
},
history: regexpHistory,
};
}
/**
* Equivalent to `new RegExp(await prompt(regexpOpts(flags), context), flags)`.
*/
export function regexp(
flags: RegExpFlags,
context = Context.WithoutActiveEditor.current,
) {
return prompt(regexpOpts(flags), context).then((x) => new RegExp(x, flags));
}
/**
* Prompts the user for a result interactively.
*/
export function interactive<T>(
compute: (input: string) => T | Thenable<T>,
reset: () => void,
options: vscode.InputBoxOptions = {},
interactive: boolean = true,
): Thenable<T> {
let result: T;
const validateInput = options.validateInput;
if (!interactive) {
return prompt(options).then((value) => compute(value));
}
return prompt({
...options,
async validateInput(input) {
const validationError = await validateInput?.(input);
if (validationError) {
return validationError;
}
try {
result = await compute(input);
return;
} catch (e) {
return `${e}`;
}
},
}).then(
() => result,
(err) => {
reset();
throw err;
},
);
}
/**
* @internal
*/
export async function manipulateSelectionsInteractively<I, R>(
_: Context,
input: Input<I>,
setInput: SetInput<R>,
interactive: boolean,
options: prompt.Options,
f: (input: string | I, selections: readonly vscode.Selection[]) => Thenable<R>,
) {
const selections = _.selections;
function execute(input: string | I) {
return _.runAsync(() => f(input, selections));
}
function undo() {
setSelections(selections);
}
if (input === undefined) {
setInput(await prompt.interactive(execute, undo, options, interactive));
} else {
await execute(input);
}
}
export type ListPair = readonly [string, string];
/**
* Prompts the user to choose one item among a list of items, and returns the
* index of the item that was picked.
*/
export function one(
items: readonly ListPair[],
init?: (quickPick: vscode.QuickPick<vscode.QuickPickItem>) => void,
options?: Readonly<{ defaultPickName?: string, defaultPick?: string }>,
context = Context.WithoutActiveEditor.current,
) {
if (options?.defaultPick != null) {
const defaultPick = options.defaultPick,
index = items.findIndex((pair) => pair[0] === defaultPick);
if (index === -1) {
const pickName = options.defaultPickName ?? "options.defaultPick",
choices = items.map((pair) => '"' + pair[0] + '"').join(", ");
return Promise.reject(new ArgumentError(`"${pickName}" must be one of ${choices}`));
if (isNaN(n)) {
return "Invalid number.";
}
return Promise.resolve(index);
}
if (opts.range && (n < opts.range[0] || n > opts.range[1])) {
return `Number out of range ${JSON.stringify(opts.range)}.`;
}
return promptInList(false, items, init ?? (() => {}), context.cancellationToken);
if (opts.integer && (n | 0) !== n) {
return `Number must be an integer.`;
}
return;
},
};
}
/**
* Equivalent to `+await prompt(numberOpts(), context)`.
*/
export function promptNumber(
opts: Parameters<typeof promptNumberOpts>[0],
context = Context.WithoutActiveEditor.current,
) {
return prompt(promptNumberOpts(opts), context).then((x) => +x);
}
/**
* Last used inputs for {@link promptRegexp}.
*/
export const regexpHistory: string[] = [];
/**
* Returns {@link vscode.InputBoxOptions} that only validate if a valid
* ECMAScript regular expression is entered.
*/
export function promptRegexpOpts(flags: promptRegexp.Flags): prompt.Options {
return {
prompt: "Regular expression",
validateInput(input) {
if (input.length === 0) {
return "RegExp cannot be empty";
}
try {
new RegExp(input, flags);
return undefined;
} catch {
return "invalid RegExp";
}
},
history: regexpHistory,
};
}
/**
* Equivalent to `new RegExp(await prompt(regexpOpts(flags), context), flags)`.
*/
export function promptRegexp(
flags: promptRegexp.Flags,
context = Context.WithoutActiveEditor.current,
) {
return prompt(promptRegexpOpts(flags), context).then((x) => new RegExp(x, flags));
}
export declare namespace promptRegexp {
export type Flag = "m" | "u" | "s" | "y" | "i" | "g";
export type Flags = Flag
| `${Flag}${Flag}`
| `${Flag}${Flag}${Flag}`
| `${Flag}${Flag}${Flag}${Flag}`;
}
/**
* Prompts the user for a result interactively.
*/
export function promptInteractively<T>(
compute: (input: string) => T | Thenable<T>,
reset: () => void,
options: vscode.InputBoxOptions = {},
interactive: boolean = true,
): Thenable<T> {
let result: T;
const validateInput = options.validateInput;
if (!interactive) {
return prompt(options).then((value) => compute(value));
}
export namespace one {
/**
* Prompts the user for actions in a menu, only hiding it when a
* cancellation is requested or `Escape` pressed.
*/
export function locked(
items: readonly (readonly [string, string, () => void])[],
init?: (quickPick: vscode.QuickPick<vscode.QuickPickItem>) => void,
cancellationToken = Context.WithoutActiveEditor.current.cancellationToken,
) {
const itemsKeys = items.map(([k, _]) => k.includes(", ") ? k.split(", ") : [...k]);
return prompt({
...options,
async validateInput(input) {
const validationError = await validateInput?.(input);
return new Promise<void>((resolve, reject) => {
const quickPick = vscode.window.createQuickPick(),
quickPickItems = [] as vscode.QuickPickItem[];
if (validationError) {
return validationError;
}
let isCaseSignificant = false;
try {
result = await compute(input);
return;
} catch (e) {
return `${e}`;
}
},
}).then(
() => result,
(err) => {
reset();
throw err;
},
);
}
for (let i = 0; i < items.length; i++) {
const [label, description] = items[i];
/**
* Calls `f` and updates the `input` with `setInput` when the prompt created
* with the given `options` is interacted with.
*
* @internal
*/
export async function manipulateSelectionsInteractively<I, R>(
_: Context,
input: Input<I>,
setInput: SetInput<R>,
interactive: boolean,
options: prompt.Options,
f: (input: string | I, selections: readonly vscode.Selection[]) => Thenable<R>,
) {
const selections = _.selections;
quickPickItems.push({ label, description });
isCaseSignificant = isCaseSignificant || label.toLowerCase() !== label;
function execute(input: string | I) {
return _.runAsync(() => f(input, selections));
}
function undo() {
setSelections(selections);
}
if (input === undefined) {
setInput(await promptInteractively(execute, undo, options, interactive));
} else {
await execute(input);
}
}
export type ListPair = readonly [string, string];
/**
* Prompts the user to choose one item among a list of items, and returns the
* index of the item that was picked.
*/
export function promptOne(
items: readonly ListPair[],
init?: (quickPick: vscode.QuickPick<vscode.QuickPickItem>) => void,
options?: Readonly<{ defaultPickName?: string, defaultPick?: string }>,
context = Context.WithoutActiveEditor.current,
) {
if (options?.defaultPick != null) {
const defaultPick = options.defaultPick,
index = items.findIndex((pair) => pair[0] === defaultPick);
if (index === -1) {
const pickName = options.defaultPickName ?? "options.defaultPick",
choices = items.map((pair) => '"' + pair[0] + '"').join(", ");
return Promise.reject(new ArgumentError(`"${pickName}" must be one of ${choices}`));
}
return Promise.resolve(index);
}
return promptInList(false, items, init ?? (() => {}), context.cancellationToken);
}
/**
* Prompts the user for actions in a menu, only hiding it when a
* cancellation is requested or `Escape` pressed.
*/
export function promptLocked(
items: readonly (readonly [string, string, () => void])[],
init?: (quickPick: vscode.QuickPick<vscode.QuickPickItem>) => void,
cancellationToken = Context.WithoutActiveEditor.current.cancellationToken,
) {
const itemsKeys = items.map(([k, _]) => k.includes(", ") ? k.split(", ") : [...k]);
return new Promise<void>((resolve, reject) => {
const quickPick = vscode.window.createQuickPick(),
quickPickItems = [] as vscode.QuickPickItem[];
let isCaseSignificant = false;
for (let i = 0; i < items.length; i++) {
const [label, description] = items[i];
quickPickItems.push({ label, description });
isCaseSignificant = isCaseSignificant || label.toLowerCase() !== label;
}
quickPick.items = quickPickItems;
quickPick.placeholder = "Press one of the below keys.";
const subscriptions = [
quickPick.onDidChangeValue((rawKey) => {
quickPick.value = "";
// This causes the menu to disappear and reappear for a frame, but
// without this the shown items don't get refreshed after the value
// change above.
quickPick.items = quickPickItems;
let key = rawKey;
if (!isCaseSignificant) {
key = key.toLowerCase();
}
quickPick.items = quickPickItems;
quickPick.placeholder = "Press one of the below keys.";
const index = itemsKeys.findIndex((x) => x.includes(key));
const subscriptions = [
quickPick.onDidChangeValue((rawKey) => {
quickPick.value = "";
if (index !== -1) {
items[index][2]();
}
}),
// This causes the menu to disappear and reappear for a frame, but
// without this the shown items don't get refreshed after the value
// change above.
quickPick.items = quickPickItems;
quickPick.onDidHide(() => {
subscriptions.splice(0).forEach((s) => s.dispose());
let key = rawKey;
resolve();
}),
if (!isCaseSignificant) {
key = key.toLowerCase();
}
quickPick.onDidAccept(() => {
subscriptions.splice(0).forEach((s) => s.dispose());
const index = itemsKeys.findIndex((x) => x.includes(key));
const picked = quickPick.selectedItems[0];
if (index !== -1) {
items[index][2]();
}
}),
try {
items.find((x) => x[1] === picked.description)![2]();
} finally {
resolve();
}
}),
quickPick.onDidHide(() => {
subscriptions.splice(0).forEach((s) => s.dispose());
cancellationToken?.onCancellationRequested(() => {
subscriptions.splice(0).forEach((s) => s.dispose());
resolve();
}),
reject(new CancellationError(CancellationError.Reason.CancellationToken));
}),
quickPick.onDidAccept(() => {
subscriptions.splice(0).forEach((s) => s.dispose());
quickPick,
];
const picked = quickPick.selectedItems[0];
init?.(quickPick);
try {
items.find((x) => x[1] === picked.description)![2]();
} finally {
resolve();
}
}),
quickPick.show();
});
}
cancellationToken?.onCancellationRequested(() => {
subscriptions.splice(0).forEach((s) => s.dispose());
/**
* Prompts the user to choose many items among a list of items, and returns a
* list of indices of picked items.
*/
export function promptMany(
items: readonly ListPair[],
init?: (quickPick: vscode.QuickPick<vscode.QuickPickItem>) => void,
context = Context.WithoutActiveEditor.current,
) {
return promptInList(true, items, init ?? (() => {}), context.cancellationToken);
}
reject(new CancellationError(CancellationError.Reason.CancellationToken));
}),
quickPick,
];
init?.(quickPick);
quickPick.show();
});
}
}
/**
* Prompts the user to choose many items among a list of items, and returns a
* list of indices of picked items.
*/
export function many(
items: readonly ListPair[],
init?: (quickPick: vscode.QuickPick<vscode.QuickPickItem>) => void,
context = Context.WithoutActiveEditor.current,
) {
return promptInList(true, items, init ?? (() => {}), context.cancellationToken);
}
/**
* Notifies an active prompt, if any, that an action has been requested.
*/
export function notifyActionRequested(action: "next" | "previous" | "clear") {
actionEvent.fire(action);
}
/**
* Notifies an active prompt, if any, that an action has been requested.
*/
export function notifyPromptActionRequested(action: "next" | "previous" | "clear") {
actionEvent.fire(action);
}
/**
@ -502,21 +505,19 @@ export function keypress(context = Context.current): Promise<string> {
);
}
export namespace keypress {
/**
* Awaits a keypress describing a register and returns the specified register.
*/
export async function forRegister(context = Context.current) {
const firstKey = await keypress(context);
/**
* Awaits a keypress describing a register and returns the specified register.
*/
export async function keypressForRegister(context = Context.current) {
const firstKey = await keypress(context);
if (firstKey !== " ") {
return context.extension.registers.get(firstKey);
}
const secondKey = await keypress(context);
return context.extension.registers.forDocument(context.document).get(secondKey);
if (firstKey !== " ") {
return context.extension.registers.get(firstKey);
}
const secondKey = await keypress(context);
return context.extension.registers.forDocument(context.document).get(secondKey);
}
function promptInList(

View File

@ -24,10 +24,10 @@ export function run(strings: string | readonly string[], context: object = {}) {
const functions: ((...args: any[]) => Thenable<unknown>)[] = [];
for (const code of strings) {
functions.push(run.compileFunction(code, Object.keys(context)));
functions.push(compileFunction(code, Object.keys(context)));
}
const parameterValues = run.parameterValues();
const parameterValues = runParameterValues();
if (isSingleStringArgument) {
return Context.WithoutActiveEditor.wrap(
@ -63,134 +63,132 @@ function ensureCacheIsPopulated() {
cachedParameters.push(vscode);
}
export namespace run {
/**
* Sets the globals available within {@link run} expressions.
*/
export function setGlobals(globals: object) {
cachedParameterNames.length = 0;
cachedParameters.length = 0;
/**
* Sets the globals available within {@link run} expressions.
*/
export function setRunGlobals(globals: object) {
cachedParameterNames.length = 0;
cachedParameters.length = 0;
globalsObject = globals;
globalsObject = globals;
}
/**
* Returns the parameter names given to dynamically run functions.
*/
export function runParameterNames() {
ensureCacheIsPopulated();
return cachedParameterNames as readonly string[];
}
/**
* Returns the parameter values given to dynamically run functions.
*/
export function runParameterValues() {
ensureCacheIsPopulated();
return cachedParameters as readonly unknown[];
}
let canRunArbitraryCode = true;
/**
* Disables usage of the {@link compileFunction} and {@link run} functions,
* preventing the execution of arbitrary user inputs.
*
* For security purposes, execution cannot be re-enabled after calling this
* function.
*/
export function disableRunFunction() {
canRunArbitraryCode = false;
}
/**
* Returns whether {@link compileFunction} and {@link run} can be used.
*/
export function runIsEnabled() {
return canRunArbitraryCode;
}
interface CompiledFunction {
(...args: any[]): Thenable<unknown>;
}
const AsyncFunction: new (...names: string[]) => CompiledFunction =
async function () {}.constructor as any,
functionCache = new Map<string, CachedFunction>();
type CachedFunction = [funct: CompiledFunction, lastAccessTimestamp: number];
/**
* A few common inputs.
*/
const safeExpressions = [
/^(\$\$?|[in]|\d+) *([=!]==?|[<>]=?|&{1,2}|\|{1,2}) *(\$\$?|[in]|\d+)$/,
/^i( + 1)?$/,
/^`\${await register\(["']\w+["'], *[i0-9]\)}` !== ["']false["']$/,
];
/**
* Compiles the given JavaScript code into a function.
*/
export function compileFunction(code: string, additionalParameterNames: readonly string[] = []) {
if (!canRunArbitraryCode && !safeExpressions.some((re) => re.test(code))) {
throw new Error("execution of arbitrary code is disabled");
}
/**
* Returns the parameter names given to dynamically run functions.
*/
export function parameterNames() {
ensureCacheIsPopulated();
const cacheId = additionalParameterNames.join(";") + code,
cached = functionCache.get(cacheId);
return cachedParameterNames as readonly string[];
if (cached !== undefined) {
cached[1] = Date.now();
return cached[0];
}
/**
* Returns the parameter values given to dynamically run functions.
*/
export function parameterValues() {
ensureCacheIsPopulated();
let func: CompiledFunction;
return cachedParameters as readonly unknown[];
try {
// Wrap code in block to allow shadowing of parameters.
func = new AsyncFunction(...runParameterNames(), ...additionalParameterNames, `{\n${code}\n}`);
} catch (e) {
throw new Error(`cannot parse function body: ${code}: ${e}`);
}
let canRunArbitraryCode = true;
functionCache.set(cacheId, [func, Date.now()]);
/**
* Disables usage of the `compileFunction` and `run` functions, preventing the
* execution of arbitrary user inputs.
*
* For security purposes, execution cannot be re-enabled after calling this
* function.
*/
export function disable() {
canRunArbitraryCode = false;
return func;
}
/**
* Removes all functions that were not used in the last n milliseconds from
* the cache.
*/
export function clearCompiledFunctionsCache(olderThanMs: number): void;
/**
* Removes all functions that were not used in the last 5 minutes from the
* cache.
*/
export function clearCompiledFunctionsCache(): void;
export function clearCompiledFunctionsCache(olderThanMs = 1000 * 60 * 5) {
if (olderThanMs === 0) {
return functionCache.clear();
}
/**
* Returns whether `compileFunction` and `run` can be used.
*/
export function isEnabled() {
return canRunArbitraryCode;
}
const olderThan = Date.now() - olderThanMs,
toDelete = [] as string[];
interface CompiledFunction {
(...args: any[]): Thenable<unknown>;
}
const AsyncFunction: new (...names: string[]) => CompiledFunction =
async function () {}.constructor as any,
functionCache = new Map<string, CachedFunction>();
type CachedFunction = [funct: CompiledFunction, lastAccessTimestamp: number];
/**
* A few common inputs.
*/
const safeExpressions = [
/^(\$\$?|[in]|\d+) *([=!]==?|[<>]=?|&{1,2}|\|{1,2}) *(\$\$?|[in]|\d+)$/,
/^i( + 1)?$/,
/^`\${await register\(["']\w+["'], *[i0-9]\)}` !== ["']false["']$/,
];
/**
* Compiles the given JavaScript code into a function.
*/
export function compileFunction(code: string, additionalParameterNames: readonly string[] = []) {
if (!canRunArbitraryCode && !safeExpressions.some((re) => re.test(code))) {
throw new Error("execution of arbitrary code is disabled");
for (const [code, value] of functionCache) {
if (value[1] < olderThan) {
toDelete.push(code);
}
const cacheId = additionalParameterNames.join(";") + code,
cached = functionCache.get(cacheId);
if (cached !== undefined) {
cached[1] = Date.now();
return cached[0];
}
let func: CompiledFunction;
try {
// Wrap code in block to allow shadowing of parameters.
func = new AsyncFunction(...parameterNames(), ...additionalParameterNames, `{\n${code}\n}`);
} catch (e) {
throw new Error(`cannot parse function body: ${code}: ${e}`);
}
functionCache.set(cacheId, [func, Date.now()]);
return func;
}
/**
* Removes all functions that were not used in the last n milliseconds from
* the cache.
*/
export function clearCache(olderThanMs: number): void;
/**
* Removes all functions that were not used in the last 5 minutes from the
* cache.
*/
export function clearCache(): void;
export function clearCache(olderThanMs = 1000 * 60 * 5) {
if (olderThanMs === 0) {
return functionCache.clear();
}
const olderThan = Date.now() - olderThanMs,
toDelete = [] as string[];
for (const [code, value] of functionCache) {
if (value[1] < olderThan) {
toDelete.push(code);
}
}
for (const code of toDelete) {
functionCache.delete(code);
}
for (const code of toDelete) {
functionCache.delete(code);
}
}
@ -315,14 +313,14 @@ export async function commands(...commands: readonly command.Any[]): Promise<any
return results;
}
export namespace command {
export declare namespace command {
/**
* A tuple given to `command`.
* A tuple given to {@link command}.
*/
export type Tuple = readonly [commandId: string, ...args: any[]];
/**
* An object given to `command`.
* An object given to {@link command}.
*/
export interface Command {
readonly command: string;
@ -390,17 +388,15 @@ export function execute(
return Context.WithoutActiveEditor.wrap(promise);
}
export namespace execute {
/**
* Disables usage of the `execute` function, preventing the execution of
* arbitrary user commands.
*
* For security purposes, execution cannot be re-enabled after calling this
* function.
*/
export function disable() {
canExecuteArbitraryCommands = false;
}
/**
* Disables usage of the {@link execute} function, preventing the execution of
* arbitrary user commands.
*
* For security purposes, execution cannot be re-enabled after calling this
* function.
*/
export function disableExecuteFunction() {
canExecuteArbitraryCommands = false;
}
function getShell() {
@ -464,28 +460,26 @@ export function switchRun(string: string, context: { $: string } & Record<string
return run("return " + string, context);
}
export namespace switchRun {
/**
* Validates the given input string. If it is invalid, an exception will be
* thrown.
*/
export function validate(string: string) {
if (string.trim().length === 0) {
throw new Error("the given string cannot be empty");
}
if (string[0] === "/") {
parseRegExpWithReplacement(string);
return;
}
if (string[0] === "#") {
if (string.slice(1).trim().length === 0) {
throw new Error("the given shell command cannot be empty");
}
return;
}
run.compileFunction("return " + string);
/**
* Validates the given input string for use in {@link switchRun}. If it is
* invalid, an exception will be thrown.
*/
export function validateForSwitchRun(string: string) {
if (string.trim().length === 0) {
throw new Error("the given string cannot be empty");
}
if (string[0] === "/") {
parseRegExpWithReplacement(string);
return;
}
if (string[0] === "#") {
if (string.slice(1).trim().length === 0) {
throw new Error("the given shell command cannot be empty");
}
return;
}
compileFunction("return " + string);
}

View File

@ -8,7 +8,8 @@ import { canMatchLineFeed, execLast, matchesStaticStrings } from "../../utils/re
/**
* Searches backward or forward for a pattern starting at the given position.
*
* @see search.backward,search.forward
* @see {@link searchBackward}
* @see {@link searchForward}
*/
export function search(
direction: Direction,
@ -17,135 +18,135 @@ export function search(
end?: vscode.Position,
) {
return direction === Direction.Backward
? search.backward(re, origin, end)
: search.forward(re, origin, end);
? searchBackward(re, origin, end)
: searchForward(re, origin, end);
}
export namespace search {
export declare namespace search {
/**
* The type of the result of a search: a `[startPosition, match]` pair if the
* search succeeded, and `undefined` otherwise.
*/
export type Result = [vscode.Position, RegExpMatchArray] | undefined;
}
/**
* Searches backward for a pattern starting at the given position.
*
* ### Example
*
* ```ts
* const [p1, [t1]] = search.backward(/\w/, new vscode.Position(0, 1))!;
*
* assert.deepStrictEqual(p1, new vscode.Position(0, 0));
* assert.strictEqual(t1, "a");
*
* const [p2, [t2]] = search.backward(/\w/, new vscode.Position(0, 2))!;
*
* assert.deepStrictEqual(p2, new vscode.Position(0, 1));
* assert.strictEqual(t2, "b");
*
* const [p3, [t3]] = search.backward(/\w+/, new vscode.Position(0, 2))!;
*
* assert.deepStrictEqual(p3, new vscode.Position(0, 0));
* assert.strictEqual(t3, "ab");
*
* assert.strictEqual(
* search.backward(/\w/, new vscode.Position(0, 0)),
* undefined,
* );
* ```
*
* With:
* ```
* abc
* ```
*/
export function backward(re: RegExp, origin: vscode.Position, end?: vscode.Position): Result {
end ??= Positions.zero;
/**
* Searches backward for a pattern starting at the given position.
*
* ### Example
*
* ```ts
* const [p1, [t1]] = searchBackward(/\w/, new vscode.Position(0, 1))!;
*
* assert.deepStrictEqual(p1, new vscode.Position(0, 0));
* assert.strictEqual(t1, "a");
*
* const [p2, [t2]] = searchBackward(/\w/, new vscode.Position(0, 2))!;
*
* assert.deepStrictEqual(p2, new vscode.Position(0, 1));
* assert.strictEqual(t2, "b");
*
* const [p3, [t3]] = searchBackward(/\w+/, new vscode.Position(0, 2))!;
*
* assert.deepStrictEqual(p3, new vscode.Position(0, 0));
* assert.strictEqual(t3, "ab");
*
* assert.strictEqual(
* searchBackward(/\w/, new vscode.Position(0, 0)),
* undefined,
* );
* ```
*
* With:
* ```
* abc
* ```
*/
export function searchBackward(re: RegExp, origin: vscode.Position, end?: vscode.Position): search.Result {
end ??= Positions.zero;
const document = Context.current.document,
searchStart = document.offsetAt(end),
searchEnd = document.offsetAt(origin),
possibleSearchLength = searchEnd - searchStart;
const document = Context.current.document,
searchStart = document.offsetAt(end),
searchEnd = document.offsetAt(origin),
possibleSearchLength = searchEnd - searchStart;
if (possibleSearchLength < 0) {
return;
}
if (possibleSearchLength > 2_000) {
const staticMatches = matchesStaticStrings(re);
if (staticMatches !== undefined) {
return searchOneOfBackward(re, staticMatches, origin, end, document);
}
if (!canMatchLineFeed(re)) {
return searchSingleLineRegExpBackward(re, origin, end, document);
}
}
return searchNaiveBackward(re, origin, end, document);
if (possibleSearchLength < 0) {
return;
}
/**
* Searches forward for a pattern starting at the given position.
*
* ### Example
*
* ```ts
* const [p1, [t1]] = search.forward(/\w/, new vscode.Position(0, 0))!;
*
* assert.deepStrictEqual(p1, new vscode.Position(0, 0));
* assert.strictEqual(t1, "a");
*
* const [p2, [t2]] = search.forward(/\w/, new vscode.Position(0, 1))!;
*
* assert.deepStrictEqual(p2, new vscode.Position(0, 1));
* assert.strictEqual(t2, "b");
*
* const [p3, [t3]] = search.forward(/\w+/, new vscode.Position(0, 1))!;
*
* assert.deepStrictEqual(p3, new vscode.Position(0, 1));
* assert.strictEqual(t3, "bc");
*
* assert.strictEqual(
* search.forward(/\w/, new vscode.Position(0, 3)),
* undefined,
* );
* ```
*
* With:
* ```
* abc
* ```
*/
export function forward(re: RegExp, origin: vscode.Position, end?: vscode.Position): Result {
const document = Context.current.document;
if (possibleSearchLength > 2_000) {
const staticMatches = matchesStaticStrings(re);
end ??= Positions.last(document);
const searchStart = document.offsetAt(origin),
searchEnd = document.offsetAt(end),
possibleSearchLength = searchEnd - searchStart;
if (possibleSearchLength < 0) {
return;
if (staticMatches !== undefined) {
return searchOneOfBackward(re, staticMatches, origin, end, document);
}
if (possibleSearchLength > 2_000) {
const staticMatches = matchesStaticStrings(re);
if (staticMatches !== undefined) {
return searchOneOfForward(re, staticMatches, origin, end, document);
}
if (!canMatchLineFeed(re)) {
return searchSingleLineRegExpForward(re, origin, end, document);
}
if (!canMatchLineFeed(re)) {
return searchSingleLineRegExpBackward(re, origin, end, document);
}
return searchNaiveForward(re, origin, end, document);
}
return searchNaiveBackward(re, origin, end, document);
}
/**
* Searches forward for a pattern starting at the given position.
*
* ### Example
*
* ```ts
* const [p1, [t1]] = searchForward(/\w/, new vscode.Position(0, 0))!;
*
* assert.deepStrictEqual(p1, new vscode.Position(0, 0));
* assert.strictEqual(t1, "a");
*
* const [p2, [t2]] = searchForward(/\w/, new vscode.Position(0, 1))!;
*
* assert.deepStrictEqual(p2, new vscode.Position(0, 1));
* assert.strictEqual(t2, "b");
*
* const [p3, [t3]] = searchForward(/\w+/, new vscode.Position(0, 1))!;
*
* assert.deepStrictEqual(p3, new vscode.Position(0, 1));
* assert.strictEqual(t3, "bc");
*
* assert.strictEqual(
* searchForward(/\w/, new vscode.Position(0, 3)),
* undefined,
* );
* ```
*
* With:
* ```
* abc
* ```
*/
export function searchForward(re: RegExp, origin: vscode.Position, end?: vscode.Position): search.Result {
const document = Context.current.document;
end ??= Positions.last(document);
const searchStart = document.offsetAt(origin),
searchEnd = document.offsetAt(end),
possibleSearchLength = searchEnd - searchStart;
if (possibleSearchLength < 0) {
return;
}
if (possibleSearchLength > 2_000) {
const staticMatches = matchesStaticStrings(re);
if (staticMatches !== undefined) {
return searchOneOfForward(re, staticMatches, origin, end, document);
}
if (!canMatchLineFeed(re)) {
return searchSingleLineRegExpForward(re, origin, end, document);
}
}
return searchNaiveForward(re, origin, end, document);
}
function maxLines(strings: readonly string[]) {

View File

@ -1,11 +1,11 @@
import * as vscode from "vscode";
import { lineByLine } from "./move";
import { lineByLineBackward, lineByLineForward } from "./move";
import { Context } from "../context";
import * as Positions from "../positions";
/**
* Returns the range of lines matching the given `RegExp` before and after
* Returns the range of lines matching the given {@link RegExp} before and after
* the given origin position.
*/
export function matchingLines(
@ -13,42 +13,40 @@ export function matchingLines(
origin: number | vscode.Position,
document = Context.current.document,
) {
const start = matchingLines.backward(re, origin, document),
end = matchingLines.forward(re, origin, document);
const start = matchingLinesBackward(re, origin, document),
end = matchingLinesForward(re, origin, document);
return new vscode.Range(start, end);
}
export namespace matchingLines {
/**
* Returns the position of the first line matching the given `RegExp`,
* starting at the `origin` line (included).
*/
export function backward(
re: RegExp,
origin: number | vscode.Position,
document = Context.current.document,
) {
return lineByLine.backward(
(text, position) => re.test(text) ? position : undefined,
typeof origin === "number" ? Positions.lineStart(origin) : origin,
document,
) ?? Positions.zero;
}
/**
* Returns the position of the last line matching the given `RegExp`, starting
* at the `origin` line (included).
*/
export function forward(
re: RegExp,
origin: number | vscode.Position,
document = Context.current.document,
) {
return lineByLine.forward(
(text, position) => re.test(text) ? position : undefined,
typeof origin === "number" ? Positions.lineStart(origin) : origin,
document,
) ?? Positions.lineStart(document.lineCount - 1);
}
/**
* Returns the position of the first line matching the given {@link RegExp},
* starting at the `origin` line (included).
*/
export function matchingLinesBackward(
re: RegExp,
origin: number | vscode.Position,
document = Context.current.document,
) {
return lineByLineBackward(
(text, position) => re.test(text) ? position : undefined,
typeof origin === "number" ? Positions.lineStart(origin) : origin,
document,
) ?? Positions.zero;
}
/**
* Returns the position of the last line matching the given {@link RegExp},
* starting at the `origin` line (included).
*/
export function matchingLinesForward(
re: RegExp,
origin: number | vscode.Position,
document = Context.current.document,
) {
return lineByLineForward(
(text, position) => re.test(text) ? position : undefined,
typeof origin === "number" ? Positions.lineStart(origin) : origin,
document,
) ?? Positions.lineStart(document.lineCount - 1);
}

View File

@ -56,46 +56,40 @@ export function moveTo(
}
}
export namespace moveTo {
/**
* Same as `moveTo`, but also ensures that the result is excluded by
* translating the resulting position by `input.length` when going backward.
*
* @see moveTo
*/
export function excluded(
direction: Direction,
string: string,
origin: vscode.Position,
document?: vscode.TextDocument,
) {
const result = moveTo(direction, string, origin, document);
/**
* Same as {@link moveTo}, but also ensures that the result is excluded by
* translating the resulting position by `input.length` when going backward.
*/
export function moveToExcluded(
direction: Direction,
string: string,
origin: vscode.Position,
document?: vscode.TextDocument,
) {
const result = moveTo(direction, string, origin, document);
if (result !== undefined && direction === Direction.Backward) {
return Positions.offset(result, string.length, document);
}
return result;
if (result !== undefined && direction === Direction.Backward) {
return Positions.offset(result, string.length, document);
}
/**
* Same as `moveTo`, but also ensures that the result is included by
* translating the resulting position by `input.length` when going forward.
*
* @see moveTo
*/
export function included(
direction: Direction,
string: string,
origin: vscode.Position,
document?: vscode.TextDocument,
) {
const result = moveTo(direction, string, origin, document);
if (result !== undefined && direction === Direction.Forward) {
return Positions.offset(result, string.length, document);
}
return result;
}
return result;
}
/**
* Same as {@link moveTo}, but also ensures that the result is included by
* translating the resulting position by `input.length` when going forward.
*/
export function moveToIncluded(
direction: Direction,
string: string,
origin: vscode.Position,
document?: vscode.TextDocument,
) {
const result = moveTo(direction, string, origin, document);
if (result !== undefined && direction === Direction.Forward) {
return Positions.offset(result, string.length, document);
}
return result;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import * as vscode from "vscode";
import { moveWhile } from "./move";
import { moveWhileByCharCode, moveWhileByCharCodeBackward, moveWhileByCharCodeForward, moveWhileReachedDocumentEdge } from "./move";
import { Context } from "../context";
import { isEmpty as lineIsEmpty } from "../lines";
import * as Positions from "../positions";
@ -8,300 +8,330 @@ import { Direction } from "../types";
import { CharSet, getCharSetFunction } from "../../utils/charset";
import { CharCodes } from "../../utils/regexp";
export namespace Range {
/**
* A function that, given a position, returns the start of the object to which
* the position belongs.
*/
export interface SeekStart {
(position: vscode.Position, inner: boolean, document: vscode.TextDocument): vscode.Position;
}
/**
* A function that, given a position, returns the end of the object to which
* the position belongs.
*
* If the whole object is being sought, the start position of the object will
* also be given.
*/
export interface SeekEnd {
(position: vscode.Position, inner: boolean,
document: vscode.TextDocument, start?: vscode.Position): vscode.Position;
}
/**
* A function that, given a position, returns the range of the object to which
* the position belongs.
*/
export interface Seek {
(position: vscode.Position, inner: boolean, document: vscode.TextDocument): vscode.Selection;
readonly start: SeekStart;
readonly end: SeekEnd;
}
/**
* Returns the range of the argument at the given position.
*/
export function argument(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
return new vscode.Selection(
argument.start(position, inner, document),
argument.end(position, inner, document),
);
}
export namespace argument {
/**
* Returns the start position of the argument at the given position.
*/
export function start(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
return toArgumentEdge(position, inner, Direction.Backward, document);
}
/**
* Returns the end position of the argument at the given position.
*/
export function end(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
return toArgumentEdge(position, inner, Direction.Forward, document);
}
}
/**
* Returns the range of lines with the same indent as the line at the given
* position.
*/
export function indent(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
// When selecting a whole indent object, scanning separately to start and
// then to end will lead to wrong results like two different indentation
// levels and skipping over blank lines more than needed. We can mitigate
// this by finding the start first and then scan from there to find the end
// of indent block.
const start = indent.start(position, inner, document),
end = indent.end(start, inner, document);
return new vscode.Selection(start, end);
}
export namespace indent {
/**
* Returns the start position of the indent block at the given position.
*/
export function start(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
return toIndentEdge(position, inner, Direction.Backward, document);
}
/**
* Returns the end position of the indent block at the given position.
*/
export function end(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
start?: vscode.Position,
) {
return toIndentEdge(start ?? position, inner, Direction.Forward, document);
}
}
/**
* Returns the range of the paragraph that wraps the given position.
*/
export function paragraph(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
let start: vscode.Position;
if (position.line + 1 < document.lineCount
&& lineIsEmpty(position.line, document) && !lineIsEmpty(position.line + 1, document)) {
// Special case: if current line is empty, check next line and select
// the NEXT paragraph if next line is not empty.
start = Positions.lineStart(position.line + 1);
} else {
start = toParagraphStart(position, document);
}
const end = toParagraphEnd(start, inner, document);
return new vscode.Selection(start, end);
}
export namespace paragraph {
/**
* Returns the start position of the paragraph that wraps the given
* position.
*/
export function start(
position: vscode.Position,
_inner: boolean,
document = Context.current.document,
) {
if (position.line > 0 && lineIsEmpty(position.line, document)) {
position = Positions.lineStart(position.line - 1); // Re-anchor to the previous line.
}
return toParagraphStart(position, document);
}
/**
* Returns the end position of the paragraph that wraps given position.
*/
export function end(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
start?: vscode.Position,
) {
if (start !== undefined) {
// It's much easier to check from start.
position = start;
}
return toParagraphEnd(position, inner, document);
}
}
/**
* Returns the range of the sentence that wraps the given position.
*/
export function sentence(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
const beforeBlank = toBeforeBlank(position, document, /* canSkipToPrevious= */ false),
start = toSentenceStart(beforeBlank, document),
end = sentence.end(start, inner, document);
return new vscode.Selection(start, end);
}
export namespace sentence {
/**
* Returns the start position of the sentence that wraps the given position.
*/
export function start(
position: vscode.Position,
_inner: boolean,
document = Context.current.document,
) {
// Special case to allow jumping to the previous sentence when position is
// at current sentence start / leading blank chars.
const beforeBlank = toBeforeBlank(position, document, /* canSkipToPrevious= */ true);
return toSentenceStart(beforeBlank, document);
}
/**
* Returns the end position of the sentence that wraps given position.
*/
export function end(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
start?: vscode.Position,
) {
if (start !== undefined) {
// It is impossible to determine if active is at leading or trailing or
// in-sentence blank characters by just looking ahead. Therefore, we
// search from the sentence start, which may be slightly less efficient
// but always accurate.
position = start;
}
if (lineIsEmpty(position.line, document)) {
// We're on an empty line which does not belong to last sentence or this
// sentence. If next line is also empty, we should just stay here.
// However, start scanning from the next line if it is not empty.
if (position.line + 1 >= document.lineCount || lineIsEmpty(position.line + 1, document)) {
return position;
} else {
position = Positions.lineStart(position.line + 1);
}
}
const isBlank = getCharSetFunction(CharSet.Blank, document);
let hadLf = false;
const innerEnd = moveWhile.byCharCode.forward(
(charCode) => {
if (charCode === CharCodes.LF) {
if (hadLf) {
return false;
}
hadLf = true;
} else {
hadLf = false;
if (punctCharCodes.indexOf(charCode) >= 0) {
return false;
}
}
return true;
},
position,
document,
);
if (moveWhile.reachedDocumentEdge) {
return innerEnd;
}
// If a sentence ends with two LFs in a row, then the first LF is part of
// the inner & outer sentence while the second LF should be excluded.
if (hadLf) {
if (inner) {
return Positions.previous(innerEnd, document)!;
}
return innerEnd;
}
if (inner) {
return innerEnd;
}
// If a sentence ends with punct char, then any blank characters after it
// but BEFORE any line breaks belongs to the outer sentence.
let col = innerEnd.character + 1;
const text = document.lineAt(innerEnd.line).text;
while (col < text.length && isBlank(text.charCodeAt(col))) {
col++;
}
if (col >= text.length) {
return Positions.lineBreak(innerEnd.line, document);
}
return new vscode.Position(innerEnd.line, col);
}
}
/**
* A function that, given a position, returns the start of the object to which
* the position belongs.
*/
export interface SeekStart {
(position: vscode.Position, inner: boolean, document: vscode.TextDocument): vscode.Position;
}
/**
* A function that, given a position, returns the end of the object to which
* the position belongs.
*
* If the whole object is being sought, the start position of the object will
* also be given.
*/
export interface SeekEnd {
(position: vscode.Position, inner: boolean,
document: vscode.TextDocument, start?: vscode.Position): vscode.Position;
}
/**
* A function that, given a position, returns the range of the object to which
* the position belongs.
*/
export interface Seek {
(position: vscode.Position, inner: boolean, document: vscode.TextDocument): vscode.Selection;
readonly start: SeekStart;
readonly end: SeekEnd;
}
/**
* Returns the range of the argument at the given position.
*/
export function argument(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
return new vscode.Selection(
argumentStart(position, inner, document),
argumentEnd(position, inner, document),
);
}
/**
* Returns the start position of the argument at the given position.
*/
export function argumentStart(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
return toArgumentEdge(position, inner, Direction.Backward, document);
}
/**
* Returns the end position of the argument at the given position.
*/
export function argumentEnd(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
return toArgumentEdge(position, inner, Direction.Forward, document);
}
export declare namespace argument {
export const start: typeof argumentStart;
export const end: typeof argumentEnd;
}
Object.defineProperties(argument, {
start: { value: argumentStart },
end: { value: argumentEnd },
});
/**
* Returns the range of lines with the same indent as the line at the given
* position.
*/
export function indent(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
// When selecting a whole indent object, scanning separately to start and
// then to end will lead to wrong results like two different indentation
// levels and skipping over blank lines more than needed. We can mitigate
// this by finding the start first and then scan from there to find the end
// of indent block.
const start = indentStart(position, inner, document),
end = indentEnd(start, inner, document);
return new vscode.Selection(start, end);
}
/**
* Returns the start position of the indent block at the given position.
*/
export function indentStart(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
return toIndentEdge(position, inner, Direction.Backward, document);
}
/**
* Returns the end position of the indent block at the given position.
*/
export function indentEnd(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
start?: vscode.Position,
) {
return toIndentEdge(start ?? position, inner, Direction.Forward, document);
}
export declare namespace indent {
export const start: typeof indentStart;
export const end: typeof indentEnd;
}
Object.defineProperties(indent, {
start: { value: indentStart },
end: { value: indentEnd },
});
/**
* Returns the range of the paragraph that wraps the given position.
*/
export function paragraph(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
let start: vscode.Position;
if (position.line + 1 < document.lineCount
&& lineIsEmpty(position.line, document) && !lineIsEmpty(position.line + 1, document)) {
// Special case: if current line is empty, check next line and select
// the NEXT paragraph if next line is not empty.
start = Positions.lineStart(position.line + 1);
} else {
start = toParagraphStart(position, document);
}
const end = toParagraphEnd(start, inner, document);
return new vscode.Selection(start, end);
}
/**
* Returns the start position of the paragraph that wraps the given
* position.
*/
export function paragraphStart(
position: vscode.Position,
_inner: boolean,
document = Context.current.document,
) {
if (position.line > 0 && lineIsEmpty(position.line, document)) {
position = Positions.lineStart(position.line - 1); // Re-anchor to the previous line.
}
return toParagraphStart(position, document);
}
/**
* Returns the end position of the paragraph that wraps given position.
*/
export function paragraphEnd(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
start?: vscode.Position,
) {
if (start !== undefined) {
// It's much easier to check from start.
position = start;
}
return toParagraphEnd(position, inner, document);
}
export declare namespace paragraph {
export const start: typeof paragraphStart;
export const end: typeof paragraphEnd;
}
Object.defineProperties(paragraph, {
start: { value: paragraphStart },
end: { value: paragraphEnd },
});
/**
* Returns the range of the sentence that wraps the given position.
*/
export function sentence(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
) {
const beforeBlank = toBeforeBlank(position, document, /* canSkipToPrevious= */ false),
start = toSentenceStart(beforeBlank, document),
end = sentenceEnd(start, inner, document);
return new vscode.Selection(start, end);
}
/**
* Returns the start position of the sentence that wraps the given position.
*/
export function sentenceStart(
position: vscode.Position,
_inner: boolean,
document = Context.current.document,
) {
// Special case to allow jumping to the previous sentence when position is
// at current sentence start / leading blank chars.
const beforeBlank = toBeforeBlank(position, document, /* canSkipToPrevious= */ true);
return toSentenceStart(beforeBlank, document);
}
/**
* Returns the end position of the sentence that wraps given position.
*/
export function sentenceEnd(
position: vscode.Position,
inner: boolean,
document = Context.current.document,
start?: vscode.Position,
) {
if (start !== undefined) {
// It is impossible to determine if active is at leading or trailing or
// in-sentence blank characters by just looking ahead. Therefore, we
// search from the sentence start, which may be slightly less efficient
// but always accurate.
position = start;
}
if (lineIsEmpty(position.line, document)) {
// We're on an empty line which does not belong to last sentence or this
// sentence. If next line is also empty, we should just stay here.
// However, start scanning from the next line if it is not empty.
if (position.line + 1 >= document.lineCount || lineIsEmpty(position.line + 1, document)) {
return position;
} else {
position = Positions.lineStart(position.line + 1);
}
}
const isBlank = getCharSetFunction(CharSet.Blank, document);
let hadLf = false;
const innerEnd = moveWhileByCharCodeForward(
(charCode) => {
if (charCode === CharCodes.LF) {
if (hadLf) {
return false;
}
hadLf = true;
} else {
hadLf = false;
if (punctCharCodes.indexOf(charCode) >= 0) {
return false;
}
}
return true;
},
position,
document,
);
if (moveWhileReachedDocumentEdge()) {
return innerEnd;
}
// If a sentence ends with two LFs in a row, then the first LF is part of
// the inner & outer sentence while the second LF should be excluded.
if (hadLf) {
if (inner) {
return Positions.previous(innerEnd, document)!;
}
return innerEnd;
}
if (inner) {
return innerEnd;
}
// If a sentence ends with punct char, then any blank characters after it
// but BEFORE any line breaks belongs to the outer sentence.
let col = innerEnd.character + 1;
const text = document.lineAt(innerEnd.line).text;
while (col < text.length && isBlank(text.charCodeAt(col))) {
col++;
}
if (col >= text.length) {
return Positions.lineBreak(innerEnd.line, document);
}
return new vscode.Position(innerEnd.line, col);
}
export declare namespace sentence {
export const start: typeof sentenceStart;
export const end: typeof sentenceEnd;
}
Object.defineProperties(sentence, {
start: { value: sentenceStart },
end: { value: sentenceEnd },
});
const punctCharCodes = new Uint32Array(Array.from(".!?¡§¶¿;՞。", (ch) => ch.charCodeAt(0)));
// ^
// I bet that's the first time you see a Greek question mark used as an actual
@ -318,7 +348,7 @@ function toArgumentEdge(
let bbalance = 0,
pbalance = 0;
const afterSkip = moveWhile.byCharCode(
const afterSkip = moveWhileByCharCode(
direction,
(charCode) => {
if (charCode === paren && pbalance === 0 && bbalance === 0) {
@ -344,7 +374,7 @@ function toArgumentEdge(
let end: vscode.Position;
if (moveWhile.reachedDocumentEdge) {
if (moveWhileReachedDocumentEdge()) {
end = afterSkip;
} else {
const charCode = document.lineAt(afterSkip.line).text.charCodeAt(afterSkip.character);
@ -370,7 +400,7 @@ function toArgumentEdge(
const isBlank = getCharSetFunction(CharSet.Blank, document);
// Exclude any surrounding whitespaces.
return moveWhile.byCharCode(-direction, isBlank, end, document);
return moveWhileByCharCode(-direction, isBlank, end, document);
}
function toIndentEdge(
@ -496,7 +526,7 @@ function toBeforeBlank(
let jumpedOverBlankLine = false,
hadLf = true;
const beforeBlank = moveWhile.byCharCode.backward(
const beforeBlank = moveWhileByCharCodeBackward(
(charCode) => {
if (charCode === CharCodes.LF) {
if (hadLf) {
@ -518,7 +548,7 @@ function toBeforeBlank(
document,
);
if (moveWhile.reachedDocumentEdge) {
if (moveWhileReachedDocumentEdge()) {
return position;
}
@ -576,7 +606,7 @@ function toSentenceStart(
let first = true,
hadLf = false;
const afterSkip = moveWhile.byCharCode.backward(
const afterSkip = moveWhileByCharCodeBackward(
(charCode) => {
if (charCode === CharCodes.LF) {
first = false;
@ -610,10 +640,10 @@ function toSentenceStart(
// If we hit two LFs or document start, the current sentence starts at the
// first non-blank character after that.
if (hadLf || moveWhile.reachedDocumentEdge) {
const start = moveWhile.byCharCode.forward(isBlank, afterSkip, document);
if (hadLf || moveWhileReachedDocumentEdge()) {
const start = moveWhileByCharCodeForward(isBlank, afterSkip, document);
if (moveWhile.reachedDocumentEdge) {
if (moveWhileReachedDocumentEdge()) {
return Positions.zero;
}

View File

@ -1,6 +1,6 @@
import * as vscode from "vscode";
import { skipEmptyLines } from "./move";
import { skipEmptyLines, skipEmptyLinesReachedDocumentEdge } from "./move";
import { Context } from "../context";
import { Direction, SelectionBehavior } from "../types";
import { CharSet, getCharSetFunction } from "../../utils/charset";
@ -53,7 +53,7 @@ export function wordBoundary(
if (isAtLineBoundary) {
const afterEmptyLines = skipEmptyLines(direction, active.line + direction, document);
if (skipEmptyLines.reachedDocumentEdge) {
if (skipEmptyLinesReachedDocumentEdge()) {
return undefined;
}

View File

@ -5,7 +5,7 @@ import { NotASelectionError } from "./errors";
import * as Positions from "./positions";
import { Direction, SelectionBehavior, Shift } from "./types";
import { execRange, splitRange } from "../utils/regexp";
import { TrackedSelection } from "../utils/tracked-selection";
import * as TrackedSelection from "../utils/tracked-selection";
export { fromCharacterMode, toCharacterMode };
@ -113,107 +113,109 @@ export function filter(
predicate: filter.Predicate<boolean> | filter.Predicate<Thenable<boolean>>,
selections?: readonly vscode.Selection[],
) {
return filter.byIndex(
return filterByIndex(
(i, selection, document) => predicate(document.getText(selection), selection, i) as any,
selections,
) as any;
}
export namespace filter {
export declare namespace filter {
/**
* A predicate passed to `filter`.
* A predicate passed to {@link filter}.
*/
export interface Predicate<T extends boolean | Thenable<boolean>> {
(text: string, selection: vscode.Selection, index: number): T;
}
}
/**
* A predicate passed to `filter.byIndex`.
*/
export interface ByIndexPredicate<T extends boolean | Thenable<boolean>> {
(index: number, selection: vscode.Selection, document: vscode.TextDocument): T;
/**
* Removes selections that do not match the given predicate.
*
* @param selections The `vscode.Selection` array to filter from, or
* `undefined` to filter the selections of the active text editor.
*/
export function filterByIndex(
predicate: filterByIndex.Predicate<boolean>,
selections?: readonly vscode.Selection[],
): vscode.Selection[];
/**
* Removes selections that do not match the given async predicate.
*
* @param selections The `vscode.Selection` array to filter from, or
* `undefined` to filter the selections of the active text editor.
*/
export function filterByIndex(
predicate: filterByIndex.Predicate<Thenable<boolean>>,
selections?: readonly vscode.Selection[],
): Thenable<vscode.Selection[]>;
export function filterByIndex(
predicate: filterByIndex.Predicate<boolean> | filterByIndex.Predicate<Thenable<boolean>>,
selections?: readonly vscode.Selection[],
) {
const context = Context.current,
document = context.document;
if (selections === undefined) {
selections = context.selections;
}
/**
* Removes selections that do not match the given predicate.
*
* @param selections The `vscode.Selection` array to filter from, or
* `undefined` to filter the selections of the active text editor.
*/
export function byIndex(
predicate: ByIndexPredicate<boolean>,
selections?: readonly vscode.Selection[],
): vscode.Selection[];
const firstSelection = selections[0],
firstResult = predicate(0, firstSelection, document);
/**
* Removes selections that do not match the given async predicate.
*
* @param selections The `vscode.Selection` array to filter from, or
* `undefined` to filter the selections of the active text editor.
*/
export function byIndex(
predicate: ByIndexPredicate<Thenable<boolean>>,
selections?: readonly vscode.Selection[],
): Thenable<vscode.Selection[]>;
export function byIndex(
predicate: ByIndexPredicate<boolean> | ByIndexPredicate<Thenable<boolean>>,
selections?: readonly vscode.Selection[],
) {
const context = Context.current,
document = context.document;
if (selections === undefined) {
selections = context.selections;
if (typeof firstResult === "boolean") {
if (selections.length === 1) {
return firstResult ? [firstResult] : [];
}
const firstSelection = selections[0],
firstResult = predicate(0, firstSelection, document);
const resultingSelections = firstResult ? [firstSelection] : [];
if (typeof firstResult === "boolean") {
if (selections.length === 1) {
return firstResult ? [firstResult] : [];
for (let i = 1; i < selections.length; i++) {
const selection = selections[i];
if (predicate(i, selection, document) as boolean) {
resultingSelections.push(selection);
}
}
const resultingSelections = firstResult ? [firstSelection] : [];
return resultingSelections;
} else {
if (selections.length === 1) {
return context.then(firstResult, (value) => value ? [firstSelection] : []);
}
for (let i = 1; i < selections.length; i++) {
const selection = selections[i];
const promises = [firstResult];
if (predicate(i, selection, document) as boolean) {
resultingSelections.push(selection);
for (let i = 1; i < selections.length; i++) {
const selection = selections[i];
promises.push(predicate(i, selection, document) as Thenable<boolean>);
}
const savedSelections = selections.slice(); // In case the original
// selections are mutated.
return context.then(Promise.all(promises), (results) => {
const resultingSelections = [];
for (let i = 0; i < results.length; i++) {
if (results[i]) {
resultingSelections.push(savedSelections[i]);
}
}
return resultingSelections;
} else {
if (selections.length === 1) {
return context.then(firstResult, (value) => value ? [firstSelection] : []);
}
});
}
}
const promises = [firstResult];
for (let i = 1; i < selections.length; i++) {
const selection = selections[i];
promises.push(predicate(i, selection, document) as Thenable<boolean>);
}
const savedSelections = selections.slice(); // In case the original
// selections are mutated.
return context.then(Promise.all(promises), (results) => {
const resultingSelections = [];
for (let i = 0; i < results.length; i++) {
if (results[i]) {
resultingSelections.push(savedSelections[i]);
}
}
return resultingSelections;
});
}
export declare namespace filterByIndex {
/**
* A predicate passed to {@link filterByIndex}.
*/
export interface Predicate<T extends boolean | Thenable<boolean>> {
(index: number, selection: vscode.Selection, document: vscode.TextDocument): T;
}
}
@ -277,108 +279,110 @@ export function map<T>(
f: map.Mapper<T | undefined> | map.Mapper<Thenable<T | undefined>>,
selections?: readonly vscode.Selection[],
) {
return map.byIndex(
return mapByIndex(
(i, selection, document) => f(document.getText(selection), selection, i),
selections,
) as any;
}
export namespace map {
export declare namespace map {
/**
* A mapper function passed to `map`.
* A mapper function passed to {@link map}.
*/
export interface Mapper<T> {
(text: string, selection: vscode.Selection, index: number): T;
}
}
/**
* A mapper function passed to `map.byIndex`.
*/
export interface ByIndexMapper<T> {
(index: number, selection: vscode.Selection, document: vscode.TextDocument): T | undefined;
/**
* Applies a function to all the given selections, and returns the array of
* all of its non-`undefined` results.
*
* @param selections The `vscode.Selection` array to map from, or `undefined`
* to map the selections of the active text editor.
*/
export function mapByIndex<T>(
f: mapByIndex.Mapper<T | undefined>,
selections?: readonly vscode.Selection[],
): T[];
/**
* Applies an async function to all the given selections, and returns the
* array of all of its non-`undefined` results.
*
* @param selections The `vscode.Selection` array to map from, or `undefined`
* to map the selections of the active text editor.
*/
export function mapByIndex<T>(
f: mapByIndex.Mapper<Thenable<T | undefined>>,
selections?: readonly vscode.Selection[],
): Thenable<T[]>;
export function mapByIndex<T>(
f: mapByIndex.Mapper<T | undefined> | mapByIndex.Mapper<Thenable<T | undefined>>,
selections?: readonly vscode.Selection[],
) {
const context = Context.current,
document = context.document;
if (selections === undefined) {
selections = context.selections;
}
/**
* Applies a function to all the given selections, and returns the array of
* all of its non-`undefined` results.
*
* @param selections The `vscode.Selection` array to map from, or `undefined`
* to map the selections of the active text editor.
*/
export function byIndex<T>(
f: ByIndexMapper<T | undefined>,
selections?: readonly vscode.Selection[],
): T[];
const firstSelection = selections[0],
firstResult = f(0, firstSelection, document);
/**
* Applies an async function to all the given selections, and returns the
* array of all of its non-`undefined` results.
*
* @param selections The `vscode.Selection` array to map from, or `undefined`
* to map the selections of the active text editor.
*/
export function byIndex<T>(
f: ByIndexMapper<Thenable<T | undefined>>,
selections?: readonly vscode.Selection[],
): Thenable<T[]>;
if (firstResult === undefined || typeof (firstResult as Thenable<T>)?.then !== "function") {
const results = firstResult !== undefined ? [firstResult as T] : [];
export function byIndex<T>(
f: ByIndexMapper<T | undefined> | ByIndexMapper<Thenable<T | undefined>>,
selections?: readonly vscode.Selection[],
) {
const context = Context.current,
document = context.document;
for (let i = 1; i < selections.length; i++) {
const selection = selections[i],
value = f(i, selection, document) as T | undefined;
if (selections === undefined) {
selections = context.selections;
if (value !== undefined) {
results.push(value);
}
}
const firstSelection = selections[0],
firstResult = f(0, firstSelection, document);
if (firstResult === undefined || typeof (firstResult as Thenable<T>)?.then !== "function") {
const results = firstResult !== undefined ? [firstResult as T] : [];
for (let i = 1; i < selections.length; i++) {
const selection = selections[i],
value = f(i, selection, document) as T | undefined;
if (value !== undefined) {
results.push(value);
}
}
return results;
} else {
if (selections.length === 1) {
return context.then(firstResult as Thenable<T | undefined>, (result) => {
return result !== undefined ? [result] : [];
});
}
const promises = [firstResult as Thenable<T | undefined>];
for (let i = 1; i < selections.length; i++) {
const selection = selections[i],
promise = f(i, selection, document) as Thenable<T | undefined>;
promises.push(promise);
}
return context.then(Promise.all(promises), (results) => {
const filteredResults = [];
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result !== undefined) {
filteredResults.push(result);
}
}
return filteredResults;
return results;
} else {
if (selections.length === 1) {
return context.then(firstResult as Thenable<T | undefined>, (result) => {
return result !== undefined ? [result] : [];
});
}
const promises = [firstResult as Thenable<T | undefined>];
for (let i = 1; i < selections.length; i++) {
const selection = selections[i],
promise = f(i, selection, document) as Thenable<T | undefined>;
promises.push(promise);
}
return context.then(Promise.all(promises), (results) => {
const filteredResults = [];
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result !== undefined) {
filteredResults.push(result);
}
}
return filteredResults;
});
}
}
export declare namespace mapByIndex {
/**
* A mapper function passed to {@link mapByIndex}.
*/
export interface Mapper<T> {
(index: number, selection: vscode.Selection, document: vscode.TextDocument): T | undefined;
}
}
@ -509,80 +513,88 @@ function mapFallbackSelections(values: (vscode.Selection | readonly [vscode.Sele
return [];
}
export namespace update {
/**
* Sets the selections of the current editor after transforming them according
* to the given function.
*/
export function byIndex(
f: map.ByIndexMapper<vscode.Selection | undefined>,
context?: Context,
): vscode.Selection[];
/**
* Sets the selections of the current editor after transforming them according
* to the given function.
*/
export function updateByIndex(
f: mapByIndex.Mapper<vscode.Selection | undefined>,
context?: Context,
): vscode.Selection[];
/**
* Sets the selections of the current editor after transforming them according
* to the given async function.
*/
export function byIndex(
f: map.ByIndexMapper<Thenable<vscode.Selection | undefined>>,
context?: Context,
): Thenable<vscode.Selection[]>;
/**
* Sets the selections of the current editor after transforming them according
* to the given async function.
*/
export function updateByIndex(
f: mapByIndex.Mapper<Thenable<vscode.Selection | undefined>>,
context?: Context,
): Thenable<vscode.Selection[]>;
export function byIndex(
f: map.ByIndexMapper<vscode.Selection | undefined>
| map.ByIndexMapper<Thenable<vscode.Selection | undefined>>,
context?: Context,
): any {
const selections = map.byIndex(f as any, context?.selections);
export function updateByIndex(
f: mapByIndex.Mapper<vscode.Selection | undefined>
| mapByIndex.Mapper<Thenable<vscode.Selection | undefined>>,
context?: Context,
): any {
const selections = mapByIndex(f as any, context?.selections);
if (Array.isArray(selections)) {
return set(selections, context);
}
return (selections as Thenable<vscode.Selection[]>).then((xs) => set(xs, context));
if (Array.isArray(selections)) {
return set(selections, context);
}
return (selections as Thenable<vscode.Selection[]>).then((xs) => set(xs, context));
}
/**
* Same as {@link update}, but additionally lets `f` return a fallback
* selection. If no selection remains after the end of the update, fallback
* selections will be used instead.
*/
export function updateWithFallback<T extends updateWithFallback.SelectionOrFallback
| Thenable<updateWithFallback.SelectionOrFallback>>(
f: map.Mapper<T>,
): T extends Thenable<updateWithFallback.SelectionOrFallback>
? Thenable<vscode.Selection[]>
: vscode.Selection[] {
const selections = map(f as any);
if (Array.isArray(selections)) {
return set(mapFallbackSelections(selections)) as any;
}
return (selections as Thenable<(vscode.Selection | readonly [vscode.Selection])[]>)
.then((values) => set(mapFallbackSelections(values))) as any;
}
export declare namespace updateWithFallback {
/**
* A possible return value for a function passed to `withFallback`. An
* array with a single selection corresponds to a fallback selection.
* A possible return value for a function passed to
* {@link updateWithFallback}. An array with a single selection corresponds to
* a fallback selection.
*/
export type SelectionOrFallback = vscode.Selection | readonly [vscode.Selection] | undefined;
}
/**
* Same as `updateSelections`, but additionally lets `f` return a fallback
* selection. If no selection remains after the end of the update, fallback
* selections will be used instead.
*/
export function withFallback<T extends SelectionOrFallback | Thenable<SelectionOrFallback>>(
f: map.Mapper<T>,
): T extends Thenable<SelectionOrFallback> ? Thenable<vscode.Selection[]> : vscode.Selection[] {
const selections = map(f as any);
/**
* Same as {@link updateWithFallback}, but does not pass the text of each
* selection.
*/
export function updateWithFallbackByIndex<
T extends updateWithFallback.SelectionOrFallback
| Thenable<updateWithFallback.SelectionOrFallback>
>(
f: mapByIndex.Mapper<T>,
): T extends Thenable<updateWithFallback.SelectionOrFallback>
? Thenable<vscode.Selection[]>
: vscode.Selection[] {
const selections = mapByIndex(f as any);
if (Array.isArray(selections)) {
return set(mapFallbackSelections(selections)) as any;
}
return (selections as Thenable<(vscode.Selection | readonly [vscode.Selection])[]>)
.then((values) => set(mapFallbackSelections(values))) as any;
if (Array.isArray(selections)) {
return set(mapFallbackSelections(selections)) as any;
}
export namespace withFallback {
/**
* Same as `withFallback`, but does not pass the text of each selection.
*/
export function byIndex<T extends SelectionOrFallback | Thenable<SelectionOrFallback>>(
f: map.ByIndexMapper<T>,
): T extends Thenable<SelectionOrFallback> ? Thenable<vscode.Selection[]> : vscode.Selection[] {
const selections = map.byIndex(f as any);
if (Array.isArray(selections)) {
return set(mapFallbackSelections(selections)) as any;
}
return (selections as Thenable<(vscode.Selection | readonly [vscode.Selection])[]>)
.then((values) => set(mapFallbackSelections(values))) as any;
}
}
return (selections as Thenable<(vscode.Selection | readonly [vscode.Selection])[]>)
.then((values) => set(mapFallbackSelections(values))) as any;
}
/**

View File

@ -1,7 +1,7 @@
import * as vscode from "vscode";
import type { Argument, InputOr, RegisterOr } from ".";
import { insert as apiInsert, Context, deindentLines, edit, indentLines, joinLines, keypress, Positions, replace, Selections, Shift } from "../api";
import { insert as apiInsert, Context, deindentLines, edit, indentLines, insertByIndex, insertByIndexWithFullLines, insertFlagsAtEdge, joinLines, keypress, Positions, replace, replaceByIndex, Selections, Shift } from "../api";
import type { Register } from "../state/registers";
import { LengthMismatchError } from "../utils/errors";
@ -65,8 +65,8 @@ export async function insert(
if (select || shift === Shift.Select) {
const textToInsert = contents.join(""),
insert = handleNewLine ? apiInsert.byIndex.withFullLines : apiInsert.byIndex,
flags = apiInsert.flagsAtEdge(where) | apiInsert.Select;
insert = handleNewLine ? insertByIndexWithFullLines : insertByIndex,
flags = insertFlagsAtEdge(where) | apiInsert.Flags.Select;
const insertedRanges = await insert(flags, () => textToInsert, selections),
allSelections = [] as vscode.Selection[],
@ -98,7 +98,7 @@ export async function insert(
}
if (where === undefined) {
Selections.set(await replace.byIndex((i) => contents![i], selections));
Selections.set(await replaceByIndex((i) => contents![i], selections));
return;
}
@ -106,13 +106,13 @@ export async function insert(
throw new Error(`"where" must be one of "active", "anchor", "start", "end", or undefined`);
}
const keepOrExtend = shift === Shift.Extend ? apiInsert.Extend : apiInsert.Keep,
flags = apiInsert.flagsAtEdge(where) | keepOrExtend;
const keepOrExtend = shift === Shift.Extend ? apiInsert.Flags.Extend : apiInsert.Flags.Keep,
flags = insertFlagsAtEdge(where) | keepOrExtend;
Selections.set(
handleNewLine
? await apiInsert.byIndex.withFullLines(flags, (i) => contents![i], selections)
: await apiInsert.byIndex(flags, (i) => contents![i], selections),
? await insertByIndexWithFullLines(flags, (i) => contents![i], selections)
: await insertByIndex(flags, (i) => contents![i], selections),
);
}
@ -452,7 +452,7 @@ function insertLinesNativelyAndCopySelections(
repetitions: number,
command: "editor.action.insertLineAfter" | "editor.action.insertLineBefore",
) {
Selections.update.byIndex(prepareSelectionForLineInsertion);
Selections.updateByIndex(prepareSelectionForLineInsertion);
if (repetitions === 1) {
return vscode.commands.executeCommand(command);

View File

@ -2,7 +2,7 @@ import * as vscode from "vscode";
import type { Argument, CommandDescriptor, RegisterOr } from ".";
import type { Context } from "../api";
import { ActiveRecording, Entry, Recorder } from "../state/recorder";
import { ActiveRecording, Cursor, Entry, Recorder } from "../state/recorder";
import type { Register } from "../state/registers";
import { ArgumentError } from "../utils/errors";
@ -105,8 +105,8 @@ export async function repeat_edit(_: Context, repetitions: number) {
const recorder = _.extension.recorder,
cursor = recorder.cursorFromEnd();
let startCursor: Recorder.Cursor | undefined,
endCursor: Recorder.Cursor | undefined;
let startCursor: Cursor | undefined,
endCursor: Cursor | undefined;
for (;;) {
if (cursor.is(Entry.ChangeTextEditorMode)) {

View File

@ -164,7 +164,7 @@ export class CommandDescriptor<Flags extends CommandDescriptor.Flags = CommandDe
}
}
export namespace CommandDescriptor {
export declare namespace CommandDescriptor {
/**
* Flags describing the behavior of some commands.
*/

View File

@ -1,7 +1,7 @@
import * as vscode from "vscode";
import type { RegisterOr } from ".";
import { Context, prompt, todo } from "../api";
import { Context, prompt, promptMany, promptOne, todo } from "../api";
import type { Register } from "../state/registers";
/**
@ -16,7 +16,7 @@ export async function setup(_: Context, register: RegisterOr<"dquote", Register.
await vscode.commands.executeCommand("workbench.action.openGlobalKeybindingsFile");
await _.switchToDocument(_.extension.editors.active!.editor.document);
const action = await prompt.one([
const action = await promptOne([
["y", "yank keybindings to register"],
["a", "append keybindings"],
["p", "prepend keybindings"],
@ -26,7 +26,7 @@ export async function setup(_: Context, register: RegisterOr<"dquote", Register.
return;
}
const keybindings = await prompt.many([
const keybindings = await promptMany([
["d", "default keybindings"],
]);

View File

@ -1,5 +1,5 @@
import type { Argument, InputOr, RegisterOr } from ".";
import { commands as apiCommands, run as apiRun, command, Context, findMenu, keypress, Menu, prompt, showLockedMenu, showMenu, validateMenu } from "../api";
import { commands as apiCommands, run as apiRun, command, compileFunction, Context, findMenu, keypressForRegister, Menu, notifyPromptActionRequested, prompt, promptNumber, runIsEnabled, showLockedMenu, showMenu, showMenuAfterDelay, validateMenu } from "../api";
import type { Extension } from "../state/extension";
import type { Register } from "../state/registers";
import { ArgumentError, CancellationError, InputError } from "../utils/errors";
@ -147,7 +147,7 @@ export async function run(
commands?: Argument<command.Any[]>,
) {
if (Array.isArray(commands)) {
if (typeof input === "string" && apiRun.isEnabled()) {
if (typeof input === "string" && runIsEnabled()) {
// Prefer "input" to the "commands" array.
} else {
return apiCommands(...commands);
@ -158,7 +158,7 @@ export async function run(
prompt: "Code to run",
validateInput(value) {
try {
apiRun.compileFunction(value);
compileFunction(value);
return;
} catch (e) {
@ -166,7 +166,7 @@ export async function run(
return `invalid syntax: ${e.message}`;
}
return e?.message ?? `${e}`;
return (e as Error)?.message ?? `${e}`;
}
},
history: runHistory,
@ -193,7 +193,7 @@ export async function run(
* @noreplay
*/
export async function selectRegister(_: Context, inputOr: InputOr<string | Register>) {
const input = await inputOr(() => keypress.forRegister(_));
const input = await inputOr(() => keypressForRegister(_));
if (typeof input === "string") {
if (input.length === 0) {
@ -292,7 +292,7 @@ export async function updateCount(
return;
}
const input = +await inputOr(() => prompt.number({ integer: true, range: [0, 1_000_000] }, _));
const input = +await inputOr(() => promptNumber({ integer: true, range: [0, 1_000_000] }, _));
InputError.validateInput(!isNaN(input), "value is not a number");
InputError.validateInput(input >= 0, "value is negative");
@ -354,7 +354,7 @@ export async function openMenu(
}
if (delay > 0) {
return showMenu.withDelay(delay, menu, pass, prefix);
return showMenuAfterDelay(delay, menu, pass, prefix);
}
return showMenu(menu, pass, prefix);
@ -373,7 +373,7 @@ export async function openMenu(
* @noreplay
*/
export function changeInput(
action: Argument<Parameters<typeof prompt.notifyActionRequested>[0]>,
action: Argument<Parameters<typeof notifyPromptActionRequested>[0]>,
) {
ArgumentError.validate(
"action",
@ -381,5 +381,5 @@ export function changeInput(
`must be "previous" or "next"`,
);
prompt.notifyActionRequested(action);
notifyPromptActionRequested(action);
}

View File

@ -1,7 +1,7 @@
import * as vscode from "vscode";
import type { Argument, Input, RegisterOr, SetInput } from ".";
import { search as apiSearch, Context, Direction, EmptySelectionsError, Positions, prompt, Selections, Shift } from "../api";
import { search as apiSearch, Context, Direction, EmptySelectionsError, manipulateSelectionsInteractively, Positions, promptRegexpOpts, Selections, Shift } from "../api";
import type { Register } from "../state/registers";
import { CharSet, getCharSetFunction } from "../utils/charset";
import { escapeForRegExp } from "../utils/regexp";
@ -35,8 +35,8 @@ export async function search(
input: Input<string | RegExp>,
setInput: SetInput<RegExp>,
) {
return prompt.manipulateSelectionsInteractively(_, input, setInput, interactive, {
...prompt.regexpOpts("mu"),
return manipulateSelectionsInteractively(_, input, setInput, interactive, {
...promptRegexpOpts("mu"),
value: (await register.get())?.[0],
}, (input, selections) => {
if (typeof input === "string") {
@ -48,7 +48,7 @@ export async function search(
const newSelections = add ? selections.slice() : [],
regexpMatches = [] as RegExpMatchArray[];
newSelections.push(...Selections.map.byIndex((_i, selection, document) => {
newSelections.push(...Selections.mapByIndex((_i, selection, document) => {
let newSelection = selection;
for (let j = 0; j < repetitions; j++) {

View File

@ -1,7 +1,7 @@
import * as vscode from "vscode";
import type { Argument, InputOr } from ".";
import { closestSurroundedBy, Context, Direction, keypress, Lines, moveTo, moveWhile, Pair, pair, Positions, prompt, Range, search, SelectionBehavior, Selections, Shift, surroundedBy, wordBoundary } from "../api";
import { closestSurroundedBy, Context, Direction, keypress, Lines, moveTo, moveToExcluded, moveWhile, moveWhileBackward, moveWhileForward, Pair, pair, Positions, prompt, Range, search, SelectionBehavior, Selections, Shift, surroundedBy, wordBoundary } from "../api";
import { CharSet } from "../utils/charset";
import { ArgumentError, assert } from "../utils/errors";
import { escapeForRegExp, execRange } from "../utils/regexp";
@ -39,7 +39,7 @@ export async function seek(
) {
const input = await inputOr(() => keypress(_));
Selections.update.byIndex((_, selection, document) => {
Selections.updateByIndex((_, selection, document) => {
let position: vscode.Position | undefined = Selections.seekFrom(selection, -direction);
for (let i = 0; i < repetitions; i++) {
@ -49,7 +49,7 @@ export async function seek(
return undefined;
}
position = moveTo.excluded(direction, input, position, document);
position = moveToExcluded(direction, input, position, document);
if (position === undefined) {
return undefined;
@ -139,7 +139,7 @@ export function enclosing(
// It only finds one next enclosing character and drags only once to its
// matching counterpart. Repetitions > 1 does exactly the same with rep=1,
// even though executing the command again will jump back and forth.
Selections.update.byIndex((_, selection, document) => {
Selections.updateByIndex((_, selection, document) => {
// First, find an enclosing char (which may be the current character).
let currentCharacter = selection.active;
@ -203,7 +203,7 @@ export function word(
) {
const charset = ws ? CharSet.NonBlank : CharSet.Word;
Selections.update.withFallback.byIndex((_i, selection) => {
Selections.updateWithFallbackByIndex((_i, selection) => {
const anchor = Selections.seekFrom(selection, direction, selection.anchor, _);
let active = Selections.seekFrom(selection, direction, selection.active, _);
@ -298,7 +298,7 @@ export async function object(
p = pair(openRe, closeRe);
if (where === "start") {
return Selections.update.byIndex((_i, selection) => {
return Selections.updateByIndex((_i, selection) => {
const startResult = p.searchOpening(Selections.activeStart(selection, _));
if (startResult === undefined) {
@ -314,7 +314,7 @@ export async function object(
}
if (where === "end") {
return Selections.update.byIndex((_i, selection) => {
return Selections.updateByIndex((_i, selection) => {
const endResult = p.searchClosing(Selections.activeEnd(selection, _));
if (endResult === undefined) {
@ -332,7 +332,7 @@ export async function object(
if (_.selectionBehavior === SelectionBehavior.Character) {
const startRe = new RegExp("^" + openRe.source, openRe.flags);
return Selections.update.byIndex((_i, selection) => {
return Selections.updateByIndex((_i, selection) => {
// If the selection behavior is character and the current character
// corresponds to the start of a pair, we select from here.
const searchStart = Selections.activeStart(selection, _),
@ -362,7 +362,7 @@ export async function object(
});
}
return Selections.update.byIndex(
return Selections.updateByIndex(
(_i, selection) => surroundedBy([p], Selections.activeStart(selection, _), !inner, _.document),
);
}
@ -376,15 +376,15 @@ export async function object(
return shiftWhere(
_,
(selection, _) => {
let start = moveWhile.backward((c) => re.test(c), selection.active, _.document),
end = moveWhile.forward((c) => re.test(c), selection.active, _.document);
let start = moveWhileBackward((c) => re.test(c), selection.active, _.document),
end = moveWhileForward((c) => re.test(c), selection.active, _.document);
if (beforeRe !== undefined) {
start = moveWhile.backward((c) => beforeRe.test(c), start, _.document);
start = moveWhileBackward((c) => beforeRe.test(c), start, _.document);
}
if (afterRe !== undefined) {
end = moveWhile.forward((c) => afterRe.test(c), end, _.document);
end = moveWhileForward((c) => afterRe.test(c), end, _.document);
}
return new vscode.Selection(start, end);
@ -454,7 +454,7 @@ export async function object(
let newSelections: vscode.Selection[];
if (where === "start") {
newSelections = Selections.map.byIndex((_i, selection, document) => {
newSelections = Selections.mapByIndex((_i, selection, document) => {
const activePosition = Selections.activePosition(selection, _.document);
let shiftTo = f.start(activePosition, inner, document);
@ -469,7 +469,7 @@ export async function object(
return Selections.shift(selection, shiftTo, shift, _);
});
} else if (where === "end") {
newSelections = Selections.map.byIndex((_i, selection, document) =>
newSelections = Selections.mapByIndex((_i, selection, document) =>
Selections.shift(
selection,
f.end(selection.active, inner, document),
@ -478,7 +478,7 @@ export async function object(
),
);
} else {
newSelections = Selections.map.byIndex((_, selection, document) =>
newSelections = Selections.mapByIndex((_, selection, document) =>
f(selection.active, inner, document),
);
}
@ -503,7 +503,7 @@ function shiftWhere(
shift: Shift,
where: "start" | "end" | undefined,
) {
Selections.update.byIndex((_, selection) => {
Selections.updateByIndex((_, selection) => {
const result = f(selection, context);
if (result === undefined) {

View File

@ -1,7 +1,7 @@
import * as vscode from "vscode";
import type { Argument } from ".";
import { firstVisibleLine as apiFirstVisibleLine, lastVisibleLine as apiLastVisibleLine, middleVisibleLine as apiMiddleVisibleLine, Context, Direction, Lines, Positions, SelectionBehavior, Selections, Shift, showMenu } from "../api";
import { firstVisibleLine as apiFirstVisibleLine, lastVisibleLine as apiLastVisibleLine, middleVisibleLine as apiMiddleVisibleLine, Context, Direction, Lines, Positions, SelectionBehavior, Selections, Shift, showMenuByName } from "../api";
import { PerEditorState } from "../state/editors";
import { unsafeSelections } from "../utils/misc";
@ -126,7 +126,7 @@ export function vertically(
);
}
const newSelections = Selections.map.byIndex((i, selection) => {
const newSelections = Selections.mapByIndex((i, selection) => {
// TODO: handle tab characters
const activeLine = isCharacterMode ? Selections.activeLine(selection) : selection.active.line,
targetLine = Lines.clamp(activeLine + repetitions * direction, document),
@ -188,7 +188,7 @@ export function vertically(
let newPosition = new vscode.Position(
targetLine,
Lines.column.character(targetLine, targetColumn, _.editor, /* roundUp= */ isCharacterMode),
Lines.character(targetLine, targetColumn, _.editor, /* roundUp= */ isCharacterMode),
);
if (isCharacterMode && shift !== Shift.Jump) {
@ -240,7 +240,7 @@ export function horizontally(
const mayNeedAdjustment = direction === Direction.Backward
&& _.selectionBehavior === SelectionBehavior.Character;
const newSelections = Selections.map.byIndex((_i, selection, document) => {
const newSelections = Selections.mapByIndex((_i, selection, document) => {
let active = selection.active === selection.start
? Selections.activeStart(selection, _)
: Selections.activeEnd(selection, _);
@ -305,7 +305,7 @@ export function to(
if (count === 0) {
// TODO: Make just merely opening the menu not count as a command execution
// and do not record it.
return showMenu.byName("goto", [argument]);
return showMenuByName("goto", [argument]);
}
return lineStart(_, count, shift);
@ -318,7 +318,7 @@ export function to(
*/
export function line_below(_: Context, count: number) {
if (count === 0 || count === 1) {
Selections.update.byIndex((_, selection) => {
Selections.updateByIndex((_, selection) => {
let line = Selections.activeLine(selection);
if (Selections.isEntireLines(selection) && !selection.isReversed) {
@ -328,7 +328,7 @@ export function line_below(_: Context, count: number) {
return new vscode.Selection(line, 0, line + 1, 0);
});
} else {
Selections.update.byIndex((_, selection, document) => {
Selections.updateByIndex((_, selection, document) => {
const lastLine = document.lineCount - 1;
let line = Math.min(Selections.activeLine(selection) + count - 1, lastLine);
@ -348,7 +348,7 @@ export function line_below(_: Context, count: number) {
*/
export function line_below_extend(_: Context, count: number) {
if (count === 0 || count === 1) {
Selections.update.byIndex((_, selection, document) => {
Selections.updateByIndex((_, selection, document) => {
const isFullLine = Selections.endsWithEntireLine(selection),
isSameLine = Selections.isSingleLine(selection),
isFullLineDiff = isFullLine && !(isSameLine && selection.isReversed) ? 1 : 0,
@ -360,7 +360,7 @@ export function line_below_extend(_: Context, count: number) {
return new vscode.Selection(anchor, active);
});
} else {
Selections.update.byIndex((_, selection, document) => {
Selections.updateByIndex((_, selection, document) => {
const activeLine = Selections.activeLine(selection),
line = Math.min(activeLine + count - 1, document.lineCount - 1),
isSameLine = Selections.isSingleLine(selection);
@ -378,7 +378,7 @@ export function line_below_extend(_: Context, count: number) {
*/
export function line_above(_: Context, count: number) {
if (count === 0 || count === 1) {
Selections.update.byIndex((_, selection) => {
Selections.updateByIndex((_, selection) => {
let line = Selections.activeLine(selection);
if (!Selections.isEntireLines(selection)) {
@ -388,7 +388,7 @@ export function line_above(_: Context, count: number) {
return new vscode.Selection(line, 0, line - 1, 0);
});
} else {
Selections.update.byIndex((_, selection) => {
Selections.updateByIndex((_, selection) => {
let line = Math.max(Selections.activeLine(selection) - count + 1, 0);
if (!Selections.isEntireLines(selection)) {
@ -405,7 +405,7 @@ export function line_above(_: Context, count: number) {
*/
export function line_above_extend(_: Context, count: number) {
if (count === 0 || count === 1) {
Selections.update.byIndex((_, selection) => {
Selections.updateByIndex((_, selection) => {
if (selection.isSingleLine) {
let line = Selections.activeLine(selection);
@ -429,7 +429,7 @@ export function line_above_extend(_: Context, count: number) {
return new vscode.Selection(selection.anchor, active);
});
} else {
Selections.update.byIndex((_, selection, document) => {
Selections.updateByIndex((_, selection, document) => {
let line = Math.max(Selections.activeLine(selection) - count, 0),
anchor = selection.anchor;
@ -485,7 +485,7 @@ export function lineStart(
return;
}
Selections.update.byIndex((_, selection) =>
Selections.updateByIndex((_, selection) =>
Selections.shift(
selection,
skipBlank
@ -541,7 +541,7 @@ export function lineEnd(
return;
}
Selections.update.byIndex((_, selection) =>
Selections.updateByIndex((_, selection) =>
mapSelection(selection, Selections.activeLine(selection)),
);
}

View File

@ -1,5 +1,5 @@
import type { Argument } from ".";
import { Context, rotate } from "../api";
import { Context, rotate, rotateContents, rotateSelections } from "../api";
/**
* Rotate selection indices and contents.
@ -39,7 +39,7 @@ export function contents(_: Context, repetitions: number, reverse: Argument<bool
repetitions = -repetitions;
}
return rotate.contentsOnly(repetitions);
return rotateContents(repetitions);
}
/**
@ -58,5 +58,5 @@ export function selections(_: Context, repetitions: number, reverse: Argument<bo
repetitions = -repetitions;
}
return rotate.selectionsOnly(repetitions);
return rotateSelections(repetitions);
}

View File

@ -1,7 +1,7 @@
import * as vscode from "vscode";
import type { Argument, Input, InputOr, RegisterOr, SetInput } from ".";
import { Context, Direction, moveWhile, Positions, prompt, SelectionBehavior, Selections, switchRun } from "../api";
import { Context, Direction, manipulateSelectionsInteractively, moveWhile, moveWhileBackward, moveWhileForward, Positions, prompt, promptOne, promptRegexpOpts, SelectionBehavior, Selections, switchRun, validateForSwitchRun } from "../api";
import { PerEditorState } from "../state/editors";
import { Mode } from "../state/modes";
import type { Register } from "../state/registers";
@ -10,7 +10,7 @@ import { AutoDisposable } from "../utils/disposables";
import { ArgumentError, EmptySelectionsError } from "../utils/errors";
import { unsafeSelections } from "../utils/misc";
import { SettingsValidator } from "../utils/settings-validator";
import { TrackedSelection } from "../utils/tracked-selection";
import * as TrackedSelection from "../utils/tracked-selection";
/**
* Interacting with selections.
@ -143,7 +143,7 @@ export async function restore_withCurrent(
add = savedSelections;
}
const type = await prompt.one([
const type = await promptOne([
["a", "Append lists"],
["u", "Union"],
["i", "Intersection"],
@ -263,9 +263,9 @@ export async function pipe(
prompt: "Expression",
validateInput(value) {
try {
switchRun.validate(value);
return void validateForSwitchRun(value);
} catch (e) {
return e?.message ?? `${e}`;
return (e as Error)?.message ?? `${e}`;
}
},
history: pipeHistory,
@ -331,20 +331,20 @@ export function filter(
const document = _.document,
strings = _.selections.map((selection) => document.getText(selection));
return prompt.manipulateSelectionsInteractively(_, input, setInput, interactive, {
return manipulateSelectionsInteractively(_, input, setInput, interactive, {
prompt: "Expression",
validateInput(value) {
try {
switchRun.validate(value);
return void validateForSwitchRun(value);
} catch (e) {
return e?.message ?? `${e}`;
return (e as Error)?.message ?? `${e}`;
}
},
value: defaultInput,
valueSelection: defaultInput ? [defaultInput.length, defaultInput.length] : undefined,
history: filterHistory,
}, (input, selections) => {
return Selections.filter.byIndex(async (i) => {
return Selections.filterByIndex(async (i) => {
const context = { $: strings[i], $$: strings, i, n: strings.length, count };
try {
@ -368,12 +368,12 @@ export function select(
input: Input<string | RegExp>,
setInput: SetInput<RegExp>,
) {
return prompt.manipulateSelectionsInteractively(
return manipulateSelectionsInteractively(
_,
input,
setInput,
interactive,
prompt.regexpOpts("mu"),
promptRegexpOpts("mu"),
(input, selections) => {
if (typeof input === "string") {
input = new RegExp(input, "mu");
@ -399,12 +399,12 @@ export function split(
input: Input<string | RegExp>,
setInput: SetInput<RegExp>,
) {
return prompt.manipulateSelectionsInteractively(
return manipulateSelectionsInteractively(
_,
input,
setInput,
interactive,
prompt.regexpOpts("mu"),
promptRegexpOpts("mu"),
(input, selections) => {
if (typeof input === "string") {
input = new RegExp(input, "mu");
@ -485,7 +485,7 @@ export function splitLines(
* @keys `a-x` (normal)
*/
export function expandToLines(_: Context) {
return Selections.update.byIndex((_, selection, document) => {
return Selections.updateByIndex((_, selection, document) => {
const start = selection.start,
end = selection.end;
@ -519,7 +519,7 @@ export function expandToLines(_: Context) {
* @keys `s-a-x` (normal)
*/
export function trimLines(_: Context) {
return Selections.update.byIndex((_, selection) => {
return Selections.updateByIndex((_, selection) => {
const start = selection.start,
end = selection.end;
@ -554,12 +554,12 @@ export function trimWhitespace(_: Context) {
const blank = getCharacters(CharSet.Blank, _.document),
isBlank = (character: string) => blank.includes(character);
return Selections.update.byIndex((_, selection, document) => {
return Selections.updateByIndex((_, selection, document) => {
const firstCharacter = selection.start,
lastCharacter = selection.end;
const start = moveWhile.forward(isBlank, firstCharacter, document),
end = moveWhile.backward(isBlank, lastCharacter, document);
const start = moveWhileForward(isBlank, firstCharacter, document),
end = moveWhileBackward(isBlank, lastCharacter, document);
if (start.isAfter(end)) {
return undefined;
@ -597,7 +597,7 @@ export function reduce(
if (empty && _.selectionBehavior !== SelectionBehavior.Character) {
if (where !== "both") {
Selections.update.byIndex((_, selection) => Selections.empty(selection[where]));
Selections.updateByIndex((_, selection) => Selections.empty(selection[where]));
} else {
Selections.set(_.selections.flatMap((selection) => {
if (selection.isEmpty) {
@ -633,7 +633,7 @@ export function reduce(
};
if (where !== "both") {
Selections.update.byIndex((_, selection) => takeWhere(selection, where));
Selections.updateByIndex((_, selection) => takeWhere(selection, where));
return;
}
@ -668,21 +668,21 @@ export function reduce(
export function changeDirection(_: Context, direction?: Direction) {
switch (direction) {
case Direction.Backward:
Selections.update.byIndex((_, selection) =>
Selections.updateByIndex((_, selection) =>
selection.isReversed || selection.isEmpty || Selections.isNonDirectional(selection)
? selection
: new vscode.Selection(selection.end, selection.start));
break;
case Direction.Forward:
Selections.update.byIndex((_, selection) =>
Selections.updateByIndex((_, selection) =>
selection.isReversed
? new vscode.Selection(selection.start, selection.end)
: selection);
break;
default:
Selections.update.byIndex((_, selection) =>
Selections.updateByIndex((_, selection) =>
selection.isEmpty || Selections.isNonDirectional(selection)
? selection
: new vscode.Selection(selection.active, selection.anchor));

View File

@ -22,13 +22,13 @@ export function activate() {
extensionPackageJSON = extensionData?.packageJSON;
if (extensionPackageJSON?.[`${extensionName}.disableArbitraryCodeExecution`]) {
api.run.disable();
api.disableRunFunction();
} else {
api.run.setGlobals({ vscode, ...api });
api.setRunGlobals({ vscode, ...api });
}
if (extensionPackageJSON?.[`${extensionName}.disableArbitraryCommandExecution`]) {
api.execute.disable();
api.disableExecuteFunction();
}
return loadCommands().then((commands) => {

View File

@ -487,8 +487,8 @@ export class PerEditorState implements vscode.Disposable {
}
}
export namespace PerEditorState {
export declare class Token<T> {
export declare namespace PerEditorState {
export class Token<T> {
private can_never_implement_this: never;
}
}

View File

@ -484,7 +484,7 @@ export class Mode {
}
}
export namespace Mode {
export declare namespace Mode {
/**
* The configuration of a `Mode` as specified in the user preferences.
*/
@ -752,7 +752,7 @@ export class Modes implements Iterable<Mode> {
}
}
export namespace Modes {
export declare namespace Modes {
export interface Configuration {
readonly [modeName: string]: Mode.Configuration;
}

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import type { Recording } from "./recorder";
import { Context, prompt, SelectionBehavior, Selections } from "../api";
import { ArgumentError, assert, EditNotAppliedError, EditorRequiredError } from "../utils/errors";
import { noUndoStops } from "../utils/misc";
import type { TrackedSelection } from "../utils/tracked-selection";
import type * as TrackedSelection from "../utils/tracked-selection";
/**
* The base class for all registers.
@ -148,7 +148,7 @@ export abstract class Register {
}
}
export namespace Register {
export declare namespace Register {
/**
* Flags describing the capabilities of a `Register`.
*/

View File

@ -1,9 +1,52 @@
import * as vscode from "vscode";
export class StatusBarSegment implements vscode.Disposable {
private readonly _statusBarItem: vscode.StatusBarItem;
private _content?: string;
public get content() {
return this._content;
}
public get statusBarItem() {
return this._statusBarItem;
}
public constructor(
public readonly name: string,
public readonly icon: string,
public readonly priority: number,
command: string | vscode.Command,
) {
this._statusBarItem =
vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, priority);
this._statusBarItem.tooltip = name;
this._statusBarItem.command = command;
}
public dispose() {
this._statusBarItem.dispose();
}
public setContent(content?: string) {
this._content = content;
if (content) {
this._statusBarItem.text = `$(${this.icon}) ${content}`;
this._statusBarItem.show();
} else {
this._statusBarItem.hide();
}
}
}
/**
* Controls the Dance status bar item.
*/
export class StatusBar implements vscode.Disposable {
public static readonly Segment = StatusBarSegment;
private readonly _segments: StatusBar.Segment[] = [];
public readonly activeModeSegment: StatusBar.Segment;
@ -48,45 +91,6 @@ export class StatusBar implements vscode.Disposable {
}
}
export namespace StatusBar {
export class Segment implements vscode.Disposable {
private readonly _statusBarItem: vscode.StatusBarItem;
private _content?: string;
public get content() {
return this._content;
}
public get statusBarItem() {
return this._statusBarItem;
}
public constructor(
public readonly name: string,
public readonly icon: string,
public readonly priority: number,
command: string | vscode.Command,
) {
this._statusBarItem =
vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, priority);
this._statusBarItem.tooltip = name;
this._statusBarItem.command = command;
}
public dispose() {
this._statusBarItem.dispose();
}
public setContent(content?: string) {
this._content = content;
if (content) {
this._statusBarItem.text = `$(${this.icon}) ${content}`;
this._statusBarItem.show();
} else {
this._statusBarItem.hide();
}
}
}
export declare namespace StatusBar {
export type Segment = StatusBarSegment;
}

View File

@ -229,7 +229,7 @@ export class AutoDisposable implements vscode.Disposable {
}
}
export namespace AutoDisposable {
export declare namespace AutoDisposable {
export const enum EventType {
OnEditorWasClosed = "editor-was-closed",
OnModeDidChange = "mode-did-change",

View File

@ -159,7 +159,7 @@ export class CancellationError extends Error {
}
}
export namespace CancellationError {
export declare namespace CancellationError {
export const enum Reason {
CancellationToken = "cancellation token was used",
PressedEscape = "user pressed <escape>",

View File

@ -456,7 +456,7 @@ export interface Node<To extends Node<To>> {
reverse(state: Node.ReverseState): To;
}
export namespace Node {
export declare namespace Node {
export type Inner<T extends Node<any>> = T extends Node<infer R> ? R : never;
export interface ReverseState {
@ -491,7 +491,7 @@ export class Sequence implements Node<Sequence> {
}
}
export namespace Sequence {
export declare namespace Sequence {
export type Node = Repeat | Anchor;
}
@ -733,7 +733,7 @@ export class CharacterSet implements Node<CharacterSet> {
CharacterSet.whitespace.alternatives, true);
}
export namespace CharacterSet {
export declare namespace CharacterSet {
export type AlternativeAtom = Raw | Escaped;
export type Alternative = AlternativeAtom | CharacterClass | [AlternativeAtom, AlternativeAtom];
}
@ -876,7 +876,7 @@ export class Lookaround extends Disjunction<Lookaround> {
}
}
export namespace Anchor {
export declare namespace Anchor {
export type Kind = AnchorKind;
}
@ -938,7 +938,7 @@ export class Repeat<T extends Repeat.Node = Repeat.Node> implements Node<Repeat>
}
}
export namespace Repeat {
export declare namespace Repeat {
export type Node = Group | Lookaround | CharacterSet | Raw | Escaped | CharacterClass | Dot
| NumericEscape | Backreference;
}

View File

@ -3,346 +3,343 @@ import * as vscode from "vscode";
import { ArgumentError } from "./errors";
import type { PerEditorState } from "../state/editors";
export namespace TrackedSelection {
/**
* Flags passed to `TrackedSelection.updateAfterDocumentChanged`.
*/
export const enum Flags {
Inclusive = 0,
/**
* Flags passed to {@link updateAfterDocumentChanged}.
*/
export const enum Flags {
Inclusive = 0,
StrictStart = 0b01,
StrictEnd = 0b10,
StrictStart = 0b01,
StrictEnd = 0b10,
Strict = 0b11,
Strict = 0b11,
EmptyExtendsForward = 0b01_00,
EmptyExtendsBackward = 0b10_00,
EmptyMoves = 0b11_00,
}
EmptyExtendsForward = 0b01_00,
EmptyExtendsBackward = 0b10_00,
EmptyMoves = 0b11_00,
}
/**
* An array of tracked selections selections.
*/
export interface Array extends Iterable<number> {
[index: number]: number;
readonly length: number;
}
/**
* An array of tracked selections selections.
*/
export interface Array extends Iterable<number> {
[index: number]: number;
readonly length: number;
}
/**
* Creates a new `TrackedSelection.Array`, reading offsets from the given
* selections in the given document.
*/
export function fromArray(
selections: readonly vscode.Selection[],
document: vscode.TextDocument,
): Array {
const trackedSelections = [] as number[];
/**
* Creates a new `TrackedSelection.Array`, reading offsets from the given
* selections in the given document.
*/
export function fromArray(
selections: readonly vscode.Selection[],
document: vscode.TextDocument,
): Array {
const trackedSelections = [] as number[];
for (let i = 0, len = selections.length; i < len; i++) {
const selection = selections[i],
anchor = selection.anchor,
active = selection.active;
for (let i = 0, len = selections.length; i < len; i++) {
const selection = selections[i],
anchor = selection.anchor,
active = selection.active;
if (anchor.line === active.line) {
const anchorOffset = document.offsetAt(anchor),
activeOffset = anchorOffset + active.character - anchor.character;
if (anchor.line === active.line) {
const anchorOffset = document.offsetAt(anchor),
activeOffset = anchorOffset + active.character - anchor.character;
trackedSelections.push(anchorOffset, activeOffset);
} else {
trackedSelections.push(document.offsetAt(anchor), document.offsetAt(active));
}
}
return trackedSelections;
}
export function restore(array: Array, index: number, document: vscode.TextDocument) {
const anchor = document.positionAt(array[index << 1]),
active = document.positionAt(array[(index << 1) | 1]);
return new vscode.Selection(anchor, active);
}
export function anchorOffset(array: Array, index: number) {
return array[index << 1];
}
export function activeOffset(array: Array, index: number) {
return array[(index << 1) | 1];
}
export function startOffset(array: Array, index: number) {
return Math.min(anchorOffset(array, index), activeOffset(array, index));
}
export function endOffset(array: Array, index: number) {
return Math.max(anchorOffset(array, index), activeOffset(array, index));
}
export function length(array: Array, index: number) {
return Math.abs(anchorOffset(array, index) - activeOffset(array, index));
}
export function setAnchorOffset(array: Array, index: number, offset: number) {
array[index << 1] = offset;
}
export function setActiveOffset(array: Array, index: number, offset: number) {
array[(index << 1) | 1] = offset;
}
export function activeIsStart(array: Array, index: number) {
return activeOffset(array, index) <= anchorOffset(array, index);
}
export function setLength(array: Array, index: number, length: number) {
const active = activeOffset(array, index),
anchor = anchorOffset(array, index);
if (active < anchor) {
setAnchorOffset(array, index, active + length);
trackedSelections.push(anchorOffset, activeOffset);
} else {
setActiveOffset(array, index, anchor + length);
trackedSelections.push(document.offsetAt(anchor), document.offsetAt(active));
}
}
export function setStartEnd(
array: Array,
index: number,
startOffset: number,
endOffset: number,
startIsActive: boolean,
) {
if (startIsActive) {
setActiveOffset(array, index, startOffset);
setAnchorOffset(array, index, endOffset);
return trackedSelections;
}
export function restore(array: Array, index: number, document: vscode.TextDocument) {
const anchor = document.positionAt(array[index << 1]),
active = document.positionAt(array[(index << 1) | 1]);
return new vscode.Selection(anchor, active);
}
export function anchorOffset(array: Array, index: number) {
return array[index << 1];
}
export function activeOffset(array: Array, index: number) {
return array[(index << 1) | 1];
}
export function startOffset(array: Array, index: number) {
return Math.min(anchorOffset(array, index), activeOffset(array, index));
}
export function endOffset(array: Array, index: number) {
return Math.max(anchorOffset(array, index), activeOffset(array, index));
}
export function length(array: Array, index: number) {
return Math.abs(anchorOffset(array, index) - activeOffset(array, index));
}
export function setAnchorOffset(array: Array, index: number, offset: number) {
array[index << 1] = offset;
}
export function setActiveOffset(array: Array, index: number, offset: number) {
array[(index << 1) | 1] = offset;
}
export function activeIsStart(array: Array, index: number) {
return activeOffset(array, index) <= anchorOffset(array, index);
}
export function setLength(array: Array, index: number, length: number) {
const active = activeOffset(array, index),
anchor = anchorOffset(array, index);
if (active < anchor) {
setAnchorOffset(array, index, active + length);
} else {
setActiveOffset(array, index, anchor + length);
}
}
export function setStartEnd(
array: Array,
index: number,
startOffset: number,
endOffset: number,
startIsActive: boolean,
) {
if (startIsActive) {
setActiveOffset(array, index, startOffset);
setAnchorOffset(array, index, endOffset);
} else {
setActiveOffset(array, index, endOffset);
setAnchorOffset(array, index, startOffset);
}
}
/**
* Returns the saved selections, restored in the given document.
*/
export function restoreArray(array: Array, document: vscode.TextDocument) {
const selections = [] as vscode.Selection[];
for (let i = 0, len = array.length >> 1; i < len; i++) {
selections.push(restore(array, i, document));
}
return selections;
}
/**
* Returns the saved selections, restored in the given document, skipping
* empty selections.
*/
export function restoreNonEmpty(array: Array, document: vscode.TextDocument) {
const selections = [] as vscode.Selection[];
for (let i = 0, len = array.length >> 1; i < len; i++) {
const anchorOffset = array[i << 1],
activeOffset = array[(i << 1) | 1];
if (anchorOffset === activeOffset) {
continue;
}
selections.push(
new vscode.Selection(document.positionAt(anchorOffset), document.positionAt(activeOffset)),
);
}
return selections;
}
/**
* Updates the underlying selection to reflect a change in its document.
*/
export function updateAfterDocumentChanged(
array: Array,
changes: readonly vscode.TextDocumentContentChangeEvent[],
flags: Flags,
) {
for (let i = 0, len = array.length; i < len; i += 2) {
let anchorOffset = array[i],
activeOffset = array[i + 1],
inclusiveActive: boolean,
inclusiveAnchor: boolean;
if (anchorOffset === activeOffset) {
// Empty selection.
inclusiveActive = (flags & Flags.EmptyExtendsForward) === Flags.EmptyExtendsForward;
inclusiveAnchor = (flags & Flags.EmptyExtendsBackward) === Flags.EmptyExtendsBackward;
} else {
setActiveOffset(array, index, endOffset);
setAnchorOffset(array, index, startOffset);
}
}
const activeIsStart = activeOffset <= anchorOffset,
anchorIsStart = activeOffset >= anchorOffset,
inclusiveStart = (flags & Flags.StrictStart) === 0,
inclusiveEnd = (flags & Flags.StrictEnd) === 0;
/**
* Returns the saved selections, restored in the given document.
*/
export function restoreArray(array: Array, document: vscode.TextDocument) {
const selections = [] as vscode.Selection[];
for (let i = 0, len = array.length >> 1; i < len; i++) {
selections.push(restore(array, i, document));
inclusiveActive = activeIsStart ? !inclusiveStart : inclusiveEnd;
inclusiveAnchor = anchorIsStart ? !inclusiveStart : inclusiveEnd;
}
return selections;
}
for (let i = 0, len = changes.length; i < len; i++) {
const change = changes[i],
diff = change.text.length - change.rangeLength,
offset = change.rangeOffset + change.rangeLength;
/**
* Returns the saved selections, restored in the given document, skipping
* empty selections.
*/
export function restoreNonEmpty(array: Array, document: vscode.TextDocument) {
const selections = [] as vscode.Selection[];
for (let i = 0, len = array.length >> 1; i < len; i++) {
const anchorOffset = array[i << 1],
activeOffset = array[(i << 1) | 1];
if (anchorOffset === activeOffset) {
continue;
if (offset < activeOffset || (inclusiveActive && offset === activeOffset)) {
activeOffset += diff;
}
selections.push(
new vscode.Selection(document.positionAt(anchorOffset), document.positionAt(activeOffset)),
);
if (offset < anchorOffset || (inclusiveAnchor && offset === anchorOffset)) {
anchorOffset += diff;
}
}
return selections;
array[i] = anchorOffset;
array[i + 1] = activeOffset;
}
}
/**
* A set of tracked selections.
*/
export class Set implements vscode.Disposable {
private readonly _onDisposed = new vscode.EventEmitter<this>();
private readonly _selections: Array;
private readonly _onDidChangeTextDocumentSubscription: vscode.Disposable;
public get onDisposed() {
return this._onDisposed.event;
}
/**
* Updates the underlying selection to reflect a change in its document.
*/
export function updateAfterDocumentChanged(
array: Array,
changes: readonly vscode.TextDocumentContentChangeEvent[],
flags: TrackedSelection.Flags,
public constructor(
selections: Array,
public readonly document: vscode.TextDocument,
public flags = Flags.Inclusive,
) {
for (let i = 0, len = array.length; i < len; i += 2) {
let anchorOffset = array[i],
activeOffset = array[i + 1],
inclusiveActive: boolean,
inclusiveAnchor: boolean;
ArgumentError.validate("selections", selections.length > 0, "selections cannot be empty");
if (anchorOffset === activeOffset) {
// Empty selection.
inclusiveActive = (flags & Flags.EmptyExtendsForward) === Flags.EmptyExtendsForward;
inclusiveAnchor = (flags & Flags.EmptyExtendsBackward) === Flags.EmptyExtendsBackward;
} else {
const activeIsStart = activeOffset <= anchorOffset,
anchorIsStart = activeOffset >= anchorOffset,
inclusiveStart = (flags & Flags.StrictStart) === 0,
inclusiveEnd = (flags & Flags.StrictEnd) === 0;
this._selections = selections;
this._onDidChangeTextDocumentSubscription =
vscode.workspace.onDidChangeTextDocument(this.updateAfterDocumentChanged, this);
}
inclusiveActive = activeIsStart ? !inclusiveStart : inclusiveEnd;
inclusiveAnchor = anchorIsStart ? !inclusiveStart : inclusiveEnd;
}
public addArray(array: Array) {
(this._selections as number[]).push(...array);
for (let i = 0, len = changes.length; i < len; i++) {
const change = changes[i],
diff = change.text.length - change.rangeLength,
offset = change.rangeOffset + change.rangeLength;
return this;
}
if (offset < activeOffset || (inclusiveActive && offset === activeOffset)) {
activeOffset += diff;
}
public addSelections(selections: readonly vscode.Selection[]) {
return this.addArray(fromArray(selections, this.document));
}
if (offset < anchorOffset || (inclusiveAnchor && offset === anchorOffset)) {
anchorOffset += diff;
}
}
array[i] = anchorOffset;
array[i + 1] = activeOffset;
}
public addSelection(selection: vscode.Selection) {
return this.addArray(fromArray([selection], this.document));
}
/**
* A set of `TrackedSelection`s.
* Updates the tracked selections to reflect a change in their document.
*
* @return whether the change was applied.
*/
export class Set implements vscode.Disposable {
private readonly _onDisposed = new vscode.EventEmitter<this>();
private readonly _selections: Array;
private readonly _onDidChangeTextDocumentSubscription: vscode.Disposable;
public get onDisposed() {
return this._onDisposed.event;
protected updateAfterDocumentChanged(e: vscode.TextDocumentChangeEvent) {
if (e.document !== this.document || e.contentChanges.length === 0) {
return false;
}
public constructor(
selections: Array,
public readonly document: vscode.TextDocument,
public flags = Flags.Inclusive,
) {
ArgumentError.validate("selections", selections.length > 0, "selections cannot be empty");
updateAfterDocumentChanged(this._selections, e.contentChanges, this.flags);
this._selections = selections;
this._onDidChangeTextDocumentSubscription =
vscode.workspace.onDidChangeTextDocument(this.updateAfterDocumentChanged, this);
}
public addArray(array: Array) {
(this._selections as number[]).push(...array);
return this;
}
public addSelections(selections: readonly vscode.Selection[]) {
return this.addArray(TrackedSelection.fromArray(selections, this.document));
}
public addSelection(selection: vscode.Selection) {
return this.addArray(TrackedSelection.fromArray([selection], this.document));
}
/**
* Updates the tracked selections to reflect a change in their document.
*
* @return whether the change was applied.
*/
protected updateAfterDocumentChanged(e: vscode.TextDocumentChangeEvent) {
if (e.document !== this.document || e.contentChanges.length === 0) {
return false;
}
updateAfterDocumentChanged(this._selections, e.contentChanges, this.flags);
return true;
}
public restore() {
return restoreArray(this._selections, this.document);
}
public restoreNonEmpty() {
return restoreNonEmpty(this._selections, this.document);
}
public dispose() {
this._onDisposed.fire(this);
this._onDisposed.dispose();
this._onDidChangeTextDocumentSubscription.dispose();
}
return true;
}
/**
* A `TrackedSelection.Set` that displays active selections using some given
* style.
*/
export class StyledSet extends Set {
private readonly _decorationType: vscode.TextEditorDecorationType;
private readonly _onDidEditorVisibilityChangeSubscription: vscode.Disposable;
public restore() {
return restoreArray(this._selections, this.document);
}
public constructor(
selections: Array,
public readonly editorState: PerEditorState,
renderOptions: vscode.DecorationRenderOptions,
) {
super(
selections, editorState.editor.document, rangeBehaviorToFlags(renderOptions.rangeBehavior));
public restoreNonEmpty() {
return restoreNonEmpty(this._selections, this.document);
}
this._decorationType = vscode.window.createTextEditorDecorationType(renderOptions);
this._onDidEditorVisibilityChangeSubscription = editorState.onVisibilityDidChange((e) =>
e.isVisible && this._updateDecorations());
this._updateDecorations();
}
public dispose() {
this._onDisposed.fire(this);
this._onDisposed.dispose();
this._onDidChangeTextDocumentSubscription.dispose();
}
}
public addArray(selections: Array) {
super.addArray(selections);
/**
* A {@link Set} that displays active selections using some given style.
*/
export class StyledSet extends Set {
private readonly _decorationType: vscode.TextEditorDecorationType;
private readonly _onDidEditorVisibilityChangeSubscription: vscode.Disposable;
for (let i = 0, len = selections.length; i < len; i += 2) {
if (selections[i] !== selections[i + 1]) {
this._updateDecorations();
break;
}
public constructor(
selections: Array,
public readonly editorState: PerEditorState,
renderOptions: vscode.DecorationRenderOptions,
) {
super(
selections, editorState.editor.document, rangeBehaviorToFlags(renderOptions.rangeBehavior));
this._decorationType = vscode.window.createTextEditorDecorationType(renderOptions);
this._onDidEditorVisibilityChangeSubscription = editorState.onVisibilityDidChange((e) =>
e.isVisible && this._updateDecorations());
this._updateDecorations();
}
public addArray(selections: Array) {
super.addArray(selections);
for (let i = 0, len = selections.length; i < len; i += 2) {
if (selections[i] !== selections[i + 1]) {
this._updateDecorations();
break;
}
return this;
}
protected updateAfterDocumentChanged(e: vscode.TextDocumentChangeEvent) {
if (!super.updateAfterDocumentChanged(e)) {
return false;
}
return this;
}
this._updateDecorations();
return true;
protected updateAfterDocumentChanged(e: vscode.TextDocumentChangeEvent) {
if (!super.updateAfterDocumentChanged(e)) {
return false;
}
public dispose() {
super.dispose();
this._updateDecorations();
this._decorationType.dispose();
this._onDidEditorVisibilityChangeSubscription.dispose();
}
return true;
}
private _updateDecorations() {
this.editorState.editor.setDecorations(this._decorationType, this.restoreNonEmpty());
}
public dispose() {
super.dispose();
this._decorationType.dispose();
this._onDidEditorVisibilityChangeSubscription.dispose();
}
private _updateDecorations() {
this.editorState.editor.setDecorations(this._decorationType, this.restoreNonEmpty());
}
}
function rangeBehaviorToFlags(rangeBehavior: vscode.DecorationRangeBehavior | undefined) {
switch (rangeBehavior) {
case vscode.DecorationRangeBehavior.ClosedOpen:
return TrackedSelection.Flags.StrictStart;
return Flags.StrictStart;
case vscode.DecorationRangeBehavior.OpenClosed:
return TrackedSelection.Flags.StrictEnd;
return Flags.StrictEnd;
case vscode.DecorationRangeBehavior.ClosedClosed:
return TrackedSelection.Flags.Strict;
return Flags.Strict;
default:
return TrackedSelection.Flags.Inclusive;
return Flags.Inclusive;
}
}

340
test/suite/api.test.ts generated
View File

@ -3,7 +3,7 @@ import * as assert from "assert";
import * as vscode from "vscode";
import { expect, ExpectedDocument } from "./utils";
import { Context, deindentLines, EmptySelectionsError, indentLines, insert, isPosition, isRange, isSelection, joinLines, moveWhile, NotASelectionError, Positions, replace, rotate, search, Select, SelectionBehavior, Selections, text } from "../../src/api";
import { Context, deindentLines, edit, EmptySelectionsError, indentLines, insert, insertByIndex, isPosition, isRange, isSelection, joinLines, mapActive, mapBoth, mapEnd, mapStart, moveWhileBackward, moveWhileForward, moveWithBackward, moveWithForward, NotASelectionError, pipe, Positions, replace, replaceByIndex, rotate, rotateContents, rotateSelections, searchBackward, searchForward, Select, SelectionBehavior, Selections, text } from "../../src/api";
import { Extension } from "../../src/state/extension";
function setSelectionBehavior(selectionBehavior: SelectionBehavior) {
@ -89,6 +89,32 @@ suite("API tests", function () {
// No expected end document.
});
test("function edit", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
hello world
^^^^^ 0
`),
after = ExpectedDocument.parseIndented(14, String.raw`
heo world
^^^ 0
`);
await before.apply(editor);
await context.runAsync(async () => {
await edit((editBuilder) => {
const start = new vscode.Position(0, 2),
end = new vscode.Position(0, 4);
editBuilder.delete(new vscode.Range(start, end));
});
});
after.assertEquals(editor);
});
test("function selectionsToCharacterMode", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
@ -247,6 +273,141 @@ suite("API tests", function () {
// No expected end document.
});
test("function mapStart", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken);
// No setup needed.
await context.runAsync(async () => {
const p1 = new vscode.Position(0, 0),
p2 = new vscode.Position(0, 1);
assert.deepStrictEqual(
mapStart(p1, (x) => x.translate(1)),
new vscode.Position(1, 0),
);
assert.deepStrictEqual(
mapStart(new vscode.Range(p1, p2), (x) => x.translate(1)),
new vscode.Range(p2, new vscode.Position(1, 0)),
);
assert.deepStrictEqual(
mapStart(new vscode.Selection(p1, p2), (x) => x.translate(1)),
new vscode.Selection(new vscode.Position(1, 0), p2),
);
assert.deepStrictEqual(
mapStart(new vscode.Selection(p2, p1), (x) => x.translate(1)),
new vscode.Selection(p2, new vscode.Position(1, 0)),
);
});
// No expected end document.
});
test("function mapEnd", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken);
// No setup needed.
await context.runAsync(async () => {
const p1 = new vscode.Position(0, 0),
p2 = new vscode.Position(0, 1);
assert.deepStrictEqual(
mapEnd(p1, (x) => x.translate(1)),
new vscode.Position(1, 0),
);
assert.deepStrictEqual(
mapEnd(new vscode.Range(p1, p2), (x) => x.translate(1)),
new vscode.Range(p1, new vscode.Position(1, 1)),
);
assert.deepStrictEqual(
mapEnd(new vscode.Selection(p1, p2), (x) => x.translate(1)),
new vscode.Selection(p1, new vscode.Position(1, 1)),
);
assert.deepStrictEqual(
mapEnd(new vscode.Selection(p2, p1), (x) => x.translate(1)),
new vscode.Selection(new vscode.Position(1, 1), p1),
);
});
// No expected end document.
});
test("function mapActive", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken);
// No setup needed.
await context.runAsync(async () => {
const p1 = new vscode.Position(0, 0),
p2 = new vscode.Position(0, 1);
assert.deepStrictEqual(
mapActive(p1, (x) => x.translate(1)),
new vscode.Position(1, 0),
);
assert.deepStrictEqual(
mapActive(new vscode.Selection(p1, p2), (x) => x.translate(1)),
new vscode.Selection(p1, new vscode.Position(1, 1)),
);
});
// No expected end document.
});
test("function mapBoth", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken);
// No setup needed.
await context.runAsync(async () => {
const p1 = new vscode.Position(0, 0),
p2 = new vscode.Position(0, 1);
assert.deepStrictEqual(
mapBoth(p1, (x) => x.translate(1)),
new vscode.Position(1, 0),
);
assert.deepStrictEqual(
mapBoth(new vscode.Range(p1, p2), (x) => x.translate(1)),
new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 1)),
);
assert.deepStrictEqual(
mapBoth(new vscode.Selection(p1, p2), (x) => x.translate(1)),
new vscode.Selection(new vscode.Position(1, 0), new vscode.Position(1, 1)),
);
assert.deepStrictEqual(
mapBoth(new vscode.Selection(p2, p1), (x) => x.translate(1)),
new vscode.Selection(new vscode.Position(1, 1), new vscode.Position(1, 0)),
);
});
// No expected end document.
});
test("function pipe", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken);
// No setup needed.
await context.runAsync(async () => {
const doubleNumbers = pipe((n) => typeof n === "number" ? n : undefined,
(n) => n * 2);
assert.deepStrictEqual(
doubleNumbers([1, "a", 2, null, 3, {}]),
[2, 4, 6],
);
});
// No expected end document.
});
});
suite("./src/api/edit/index.ts", function () {
@ -276,7 +437,7 @@ suite("API tests", function () {
after.assertEquals(editor);
});
test("function byIndex", async function () {
test("function insertByIndex", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -295,13 +456,13 @@ suite("API tests", function () {
await before.apply(editor);
await context.runAsync(async () => {
Selections.set(await insert.byIndex(insert.Start, (i) => `${i + 1}`));
Selections.set(await insertByIndex(insert.Start, (i) => `${i + 1}`));
});
after.assertEquals(editor);
});
test("function byIndex#1", async function () {
test("function insertByIndex#1", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -320,13 +481,13 @@ suite("API tests", function () {
await before.apply(editor);
await context.runAsync(async () => {
Selections.set(await insert.byIndex(insert.Start | insert.Select, (i) => `${i + 1}`));
Selections.set(await insertByIndex(insert.Start | insert.Select, (i) => `${i + 1}`));
});
after.assertEquals(editor);
});
test("function byIndex#2", async function () {
test("function insertByIndex#2", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -345,13 +506,13 @@ suite("API tests", function () {
await before.apply(editor);
await context.runAsync(async () => {
Selections.set(await insert.byIndex(insert.Start | insert.Extend, (i) => `${i + 1}`));
Selections.set(await insertByIndex(insert.Start | insert.Extend, (i) => `${i + 1}`));
});
after.assertEquals(editor);
});
test("function byIndex#3", async function () {
test("function insertByIndex#3", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -370,13 +531,13 @@ suite("API tests", function () {
await before.apply(editor);
await context.runAsync(async () => {
Selections.set(await insert.byIndex(insert.End, (i) => `${i + 1}`));
Selections.set(await insertByIndex(insert.End, (i) => `${i + 1}`));
});
after.assertEquals(editor);
});
test("function byIndex#4", async function () {
test("function insertByIndex#4", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -395,13 +556,13 @@ suite("API tests", function () {
await before.apply(editor);
await context.runAsync(async () => {
Selections.set(await insert.byIndex(insert.End | insert.Select, (i) => `${i + 1}`));
Selections.set(await insertByIndex(insert.End | insert.Select, (i) => `${i + 1}`));
});
after.assertEquals(editor);
});
test("function byIndex#5", async function () {
test("function insertByIndex#5", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -420,7 +581,7 @@ suite("API tests", function () {
await before.apply(editor);
await context.runAsync(async () => {
Selections.set(await insert.byIndex(insert.End | insert.Extend, (i) => `${i + 1}`));
Selections.set(await insertByIndex(insert.End | insert.Extend, (i) => `${i + 1}`));
});
after.assertEquals(editor);
@ -451,7 +612,7 @@ suite("API tests", function () {
after.assertEquals(editor);
});
test("function byIndex#6", async function () {
test("function replaceByIndex", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -470,7 +631,7 @@ suite("API tests", function () {
await before.apply(editor);
await context.runAsync(async () => {
await replace.byIndex((i) => `${i + 1}`);
await replaceByIndex((i) => `${i + 1}`);
});
after.assertEquals(editor);
@ -501,7 +662,32 @@ suite("API tests", function () {
after.assertEquals(editor);
});
test("function selectionsOnly", async function () {
test("function rotateContents", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
a b c
^ 0
^ 1
^ 2
`),
after = ExpectedDocument.parseIndented(14, String.raw`
b c a
^ 0
^ 1
^ 2
`);
await before.apply(editor);
await context.runAsync(async () => {
await rotateContents(1);
});
after.assertEquals(editor);
});
test("function rotateSelections", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -520,7 +706,7 @@ suite("API tests", function () {
await before.apply(editor);
await context.runAsync(async () => {
rotate.selectionsOnly(1);
rotateSelections(1);
});
after.assertEquals(editor);
@ -530,7 +716,7 @@ suite("API tests", function () {
suite("./src/api/search/index.ts", function () {
test("function backward", async function () {
test("function searchBackward", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -540,23 +726,23 @@ suite("API tests", function () {
await before.apply(editor);
await context.runAsync(async () => {
const [p1, [t1]] = search.backward(/\w/, new vscode.Position(0, 1))!;
const [p1, [t1]] = searchBackward(/\w/, new vscode.Position(0, 1))!;
assert.deepStrictEqual(p1, new vscode.Position(0, 0));
assert.strictEqual(t1, "a");
const [p2, [t2]] = search.backward(/\w/, new vscode.Position(0, 2))!;
const [p2, [t2]] = searchBackward(/\w/, new vscode.Position(0, 2))!;
assert.deepStrictEqual(p2, new vscode.Position(0, 1));
assert.strictEqual(t2, "b");
const [p3, [t3]] = search.backward(/\w+/, new vscode.Position(0, 2))!;
const [p3, [t3]] = searchBackward(/\w+/, new vscode.Position(0, 2))!;
assert.deepStrictEqual(p3, new vscode.Position(0, 0));
assert.strictEqual(t3, "ab");
assert.strictEqual(
search.backward(/\w/, new vscode.Position(0, 0)),
searchBackward(/\w/, new vscode.Position(0, 0)),
undefined,
);
});
@ -564,7 +750,7 @@ suite("API tests", function () {
// No expected end document.
});
test("function forward", async function () {
test("function searchForward", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -574,23 +760,23 @@ suite("API tests", function () {
await before.apply(editor);
await context.runAsync(async () => {
const [p1, [t1]] = search.forward(/\w/, new vscode.Position(0, 0))!;
const [p1, [t1]] = searchForward(/\w/, new vscode.Position(0, 0))!;
assert.deepStrictEqual(p1, new vscode.Position(0, 0));
assert.strictEqual(t1, "a");
const [p2, [t2]] = search.forward(/\w/, new vscode.Position(0, 1))!;
const [p2, [t2]] = searchForward(/\w/, new vscode.Position(0, 1))!;
assert.deepStrictEqual(p2, new vscode.Position(0, 1));
assert.strictEqual(t2, "b");
const [p3, [t3]] = search.forward(/\w+/, new vscode.Position(0, 1))!;
const [p3, [t3]] = searchForward(/\w+/, new vscode.Position(0, 1))!;
assert.deepStrictEqual(p3, new vscode.Position(0, 1));
assert.strictEqual(t3, "bc");
assert.strictEqual(
search.forward(/\w/, new vscode.Position(0, 3)),
searchForward(/\w/, new vscode.Position(0, 3)),
undefined,
);
});
@ -816,7 +1002,49 @@ suite("API tests", function () {
suite("./src/api/search/move.ts", function () {
test("function backward", async function () {
test("function moveWithBackward", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
1234578
`);
await before.apply(editor);
await context.runAsync(async () => {
// Go backward as long as the previous character is equal to the current
// character minus one.
assert.deepStrictEqual(
moveWithBackward((c, i) => +c === i - 1 ? +c : undefined,
9, new vscode.Position(0, 7)),
new vscode.Position(0, 5),
);
});
// No expected end document.
});
test("function moveWithForward", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
1234578
`);
await before.apply(editor);
await context.runAsync(async () => {
assert.deepStrictEqual(
moveWithForward((c, i) => +c === i + 1 ? +c : undefined,
0, new vscode.Position(0, 0)),
new vscode.Position(0, 5),
);
});
// No expected end document.
});
test("function moveWhileBackward", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -827,17 +1055,17 @@ suite("API tests", function () {
await context.runAsync(async () => {
assert.deepStrictEqual(
moveWhile.backward((c) => /\w/.test(c), new vscode.Position(0, 3)),
moveWhileBackward((c) => /\w/.test(c), new vscode.Position(0, 3)),
new vscode.Position(0, 0),
);
assert.deepStrictEqual(
moveWhile.backward((c) => c === "c", new vscode.Position(0, 3)),
moveWhileBackward((c) => c === "c", new vscode.Position(0, 3)),
new vscode.Position(0, 2),
);
assert.deepStrictEqual(
moveWhile.backward((c) => c === "b", new vscode.Position(0, 3)),
moveWhileBackward((c) => c === "b", new vscode.Position(0, 3)),
new vscode.Position(0, 3),
);
});
@ -845,7 +1073,7 @@ suite("API tests", function () {
// No expected end document.
});
test("function forward", async function () {
test("function moveWhileForward", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
@ -856,17 +1084,17 @@ suite("API tests", function () {
await context.runAsync(async () => {
assert.deepStrictEqual(
moveWhile.forward((c) => /\w/.test(c), new vscode.Position(0, 0)),
moveWhileForward((c) => /\w/.test(c), new vscode.Position(0, 0)),
new vscode.Position(0, 3),
);
assert.deepStrictEqual(
moveWhile.forward((c) => c === "a", new vscode.Position(0, 0)),
moveWhileForward((c) => c === "a", new vscode.Position(0, 0)),
new vscode.Position(0, 1),
);
assert.deepStrictEqual(
moveWhile.forward((c) => c === "b", new vscode.Position(0, 0)),
moveWhileForward((c) => c === "b", new vscode.Position(0, 0)),
new vscode.Position(0, 0),
);
});
@ -962,6 +1190,48 @@ suite("API tests", function () {
// No expected end document.
});
test("function map", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
foo 123
^^^ 0
^^^ 1
`);
await before.apply(editor);
await context.runAsync(async () => {
assert.deepStrictEqual(
Selections.map((text) => isNaN(+text) ? undefined : +text),
[123],
);
});
// No expected end document.
});
test("function map#1", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),
before = ExpectedDocument.parseIndented(14, String.raw`
foo 123
^^^ 0
^^^ 1
`);
await before.apply(editor);
await context.runAsync(async () => {
assert.deepStrictEqual(
await Selections.map(async (text) => isNaN(+text) ? undefined : +text),
[123],
);
});
// No expected end document.
});
test("function update", async function () {
const editorState = extension.editors.getState(editor)!,
context = new Context(editorState, cancellationToken),