mirror of
https://github.com/VSCodeVim/Vim.git
synced 2024-09-19 08:07:28 +03:00
Implement "smart quotes", part of the targets.vim
plugin (#7025)
This can be toggled with `vim.smartQuotes.enable`. See the PR for usage details. Refs #601
This commit is contained in:
parent
941987238e
commit
f3a9f6a317
20
package.json
20
package.json
@ -739,6 +739,26 @@
|
||||
"markdownDescription": "`#editor.lineNumbers#` is determined by the active Vim mode, absolute when in insert mode, relative otherwise.",
|
||||
"default": false
|
||||
},
|
||||
"vim.targets.enable": {
|
||||
"type": "boolean",
|
||||
"markdownDescription": "Enable [targets.vim](https://github.com/wellle/targets.vim#quote-text-objects) plugin (not fully implmeneted yet).",
|
||||
"default": false
|
||||
},
|
||||
"vim.targets.smartQuotes.enable": {
|
||||
"type": "boolean",
|
||||
"markdownDescription": "Enable the smart quotes movements from [targets.vim](https://github.com/wellle/targets.vim#quote-text-objects).",
|
||||
"default": true
|
||||
},
|
||||
"vim.targets.smartQuotes.breakThroughLines": {
|
||||
"type": "boolean",
|
||||
"markdownDescription": "Whether to break through lines when using [n]ext/[l]ast motion, see [targets.vim#next-and-last-quote](https://github.com/wellle/targets.vim#next-and-last-quote).",
|
||||
"default": true
|
||||
},
|
||||
"vim.targets.smartQuotes.aIncludesSurroundingSpaces": {
|
||||
"type": "boolean",
|
||||
"markdownDescription": "Whether to use default Vim behavior when using `a` (e.g. `da'`) which include surrounding spaces, or not, as for other text objects.",
|
||||
"default": true
|
||||
},
|
||||
"vim.sneak": {
|
||||
"type": "boolean",
|
||||
"markdownDescription": "Enable the [Sneak](https://github.com/justinmk/vim-sneak) plugin for Vim.",
|
||||
|
@ -19,3 +19,4 @@ import './plugins/easymotion/registerMoveActions';
|
||||
import './plugins/sneak';
|
||||
import './plugins/replaceWithRegister';
|
||||
import './plugins/surround';
|
||||
import './plugins/targets/targets';
|
||||
|
@ -5,3 +5,4 @@ import './plugins/easymotion/registerMoveActions';
|
||||
import './plugins/sneak';
|
||||
import './plugins/replaceWithRegister';
|
||||
import './plugins/surround';
|
||||
import './plugins/targets/targets';
|
||||
|
@ -27,6 +27,8 @@ import { sorted } from '../common/motion/position';
|
||||
import { WordType } from '../textobject/word';
|
||||
import { CommandInsertAtCursor } from './commands/actions';
|
||||
import { SearchDirection } from '../vimscript/pattern';
|
||||
import { SmartQuoteMatcher, WhichQuotes } from './plugins/targets/smartQuotesMatcher';
|
||||
import { useSmartQuotes } from './plugins/targets/targetsConfig';
|
||||
|
||||
/**
|
||||
* A movement is something like 'h', 'k', 'w', 'b', 'gg', etc.
|
||||
@ -1964,9 +1966,11 @@ export class MoveAroundSquareBracket extends MoveInsideCharacter {
|
||||
// TODO: Shouldn't this be a TextObject? A clearer delineation between motions and objects should be made.
|
||||
export abstract class MoveQuoteMatch extends BaseMovement {
|
||||
override modes = [Mode.Normal, Mode.Visual, Mode.VisualBlock];
|
||||
protected readonly anyQuote: boolean = false;
|
||||
protected abstract readonly charToMatch: '"' | "'" | '`';
|
||||
protected includeQuotes = false;
|
||||
override isJump = true;
|
||||
readonly which: WhichQuotes = 'current';
|
||||
|
||||
// HACK: surround uses these classes, but does not want trailing whitespace to be included
|
||||
private adjustForTrailingWhitespace: boolean = true;
|
||||
@ -1996,41 +2000,89 @@ export abstract class MoveQuoteMatch extends BaseMovement {
|
||||
this.adjustForTrailingWhitespace = false;
|
||||
}
|
||||
|
||||
const text = vimState.document.lineAt(position).text;
|
||||
const quoteMatcher = new QuoteMatcher(this.charToMatch, text);
|
||||
const quoteIndices = quoteMatcher.surroundingQuotes(position.character);
|
||||
if (quoteIndices === undefined) {
|
||||
return failedMovement(vimState);
|
||||
}
|
||||
if (useSmartQuotes()) {
|
||||
const quoteMatcher = new SmartQuoteMatcher(
|
||||
this.anyQuote ? 'any' : this.charToMatch,
|
||||
vimState.document
|
||||
);
|
||||
const res = quoteMatcher.smartSurroundingQuotes(position, this.which);
|
||||
|
||||
let [start, end] = quoteIndices;
|
||||
|
||||
if (!this.includeQuotes) {
|
||||
// Don't include the quotes
|
||||
start++;
|
||||
end--;
|
||||
} else if (this.adjustForTrailingWhitespace) {
|
||||
// Include trailing whitespace if there is any...
|
||||
const trailingWhitespace = text.substring(end + 1).search(/\S|$/);
|
||||
if (trailingWhitespace > 0) {
|
||||
end += trailingWhitespace;
|
||||
} else {
|
||||
// ...otherwise include leading whitespace
|
||||
start = text.substring(0, start).search(/\s*$/);
|
||||
if (res === undefined) {
|
||||
return failedMovement(vimState);
|
||||
}
|
||||
let { start, stop, lineText } = res;
|
||||
|
||||
if (!this.includeQuotes) {
|
||||
// Don't include the quotes
|
||||
start = start.translate({ characterDelta: 1 });
|
||||
stop = stop.translate({ characterDelta: -1 });
|
||||
} else if (
|
||||
this.adjustForTrailingWhitespace &&
|
||||
configuration.targets.smartQuotes.aIncludesSurroundingSpaces
|
||||
) {
|
||||
// Include trailing whitespace if there is any...
|
||||
const trailingWhitespace = lineText.substring(stop.character + 1).search(/\S|$/);
|
||||
if (trailingWhitespace > 0) {
|
||||
stop = stop.translate({ characterDelta: trailingWhitespace });
|
||||
} else {
|
||||
// ...otherwise include leading whitespace
|
||||
start = start.with({ character: lineText.substring(0, start.character).search(/\s*$/) });
|
||||
}
|
||||
}
|
||||
|
||||
if (!isVisualMode(vimState.currentMode) && position.isBefore(start)) {
|
||||
vimState.recordedState.operatorPositionDiff = start.subtract(position);
|
||||
} else if (!isVisualMode(vimState.currentMode) && position.isAfter(stop)) {
|
||||
if (position.line === stop.line) {
|
||||
vimState.recordedState.operatorPositionDiff = stop.getRight().subtract(position);
|
||||
} else {
|
||||
vimState.recordedState.operatorPositionDiff = start.subtract(position);
|
||||
}
|
||||
}
|
||||
|
||||
vimState.cursorStartPosition = start;
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
} else {
|
||||
const text = vimState.document.lineAt(position).text;
|
||||
const quoteMatcher = new QuoteMatcher(this.charToMatch, text);
|
||||
const quoteIndices = quoteMatcher.surroundingQuotes(position.character);
|
||||
|
||||
if (quoteIndices === undefined) {
|
||||
return failedMovement(vimState);
|
||||
}
|
||||
|
||||
let [start, end] = quoteIndices;
|
||||
|
||||
if (!this.includeQuotes) {
|
||||
// Don't include the quotes
|
||||
start++;
|
||||
end--;
|
||||
} else if (this.adjustForTrailingWhitespace) {
|
||||
// Include trailing whitespace if there is any...
|
||||
const trailingWhitespace = text.substring(end + 1).search(/\S|$/);
|
||||
if (trailingWhitespace > 0) {
|
||||
end += trailingWhitespace;
|
||||
} else {
|
||||
// ...otherwise include leading whitespace
|
||||
start = text.substring(0, start).search(/\s*$/);
|
||||
}
|
||||
}
|
||||
|
||||
const startPos = new Position(position.line, start);
|
||||
const endPos = new Position(position.line, end);
|
||||
|
||||
if (!isVisualMode(vimState.currentMode) && position.isBefore(startPos)) {
|
||||
vimState.recordedState.operatorPositionDiff = startPos.subtract(position);
|
||||
}
|
||||
|
||||
return {
|
||||
start: startPos,
|
||||
stop: endPos,
|
||||
};
|
||||
}
|
||||
|
||||
const startPos = new Position(position.line, start);
|
||||
const endPos = new Position(position.line, end);
|
||||
|
||||
if (!isVisualMode(vimState.currentMode) && position.isBefore(startPos)) {
|
||||
vimState.recordedState.operatorPositionDiff = startPos.subtract(position);
|
||||
}
|
||||
|
||||
return {
|
||||
start: startPos,
|
||||
stop: endPos,
|
||||
};
|
||||
}
|
||||
|
||||
public override async execActionForOperator(
|
||||
|
155
src/actions/plugins/targets/smartQuotes.ts
Normal file
155
src/actions/plugins/targets/smartQuotes.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { RegisterAction } from '../../base';
|
||||
import { Mode } from '../../../mode/mode';
|
||||
import { MoveQuoteMatch } from '../../motion';
|
||||
|
||||
abstract class SmartQuotes extends MoveQuoteMatch {
|
||||
override modes = [Mode.Normal, Mode.Visual, Mode.VisualBlock];
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveAroundNextSingleQuotes extends SmartQuotes {
|
||||
keys = ['a', 'n', "'"];
|
||||
readonly charToMatch = "'";
|
||||
override readonly which = 'next';
|
||||
override includeQuotes = true;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveInsideNextSingleQuotes extends SmartQuotes {
|
||||
keys = ['i', 'n', "'"];
|
||||
readonly charToMatch = "'";
|
||||
override readonly which = 'next';
|
||||
override includeQuotes = false;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveAroundLastSingleQuotes extends SmartQuotes {
|
||||
keys = ['a', 'l', "'"];
|
||||
readonly charToMatch = "'";
|
||||
override readonly which = 'last';
|
||||
override includeQuotes = true;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveInsideLastSingleQuotes extends SmartQuotes {
|
||||
keys = ['i', 'l', "'"];
|
||||
readonly charToMatch = "'";
|
||||
override readonly which = 'last';
|
||||
override includeQuotes = false;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveAroundNextDoubleQuotes extends SmartQuotes {
|
||||
keys = ['a', 'n', '"'];
|
||||
readonly charToMatch = '"';
|
||||
override readonly which = 'next';
|
||||
override includeQuotes = true;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveInsideNextDoubleQuotes extends SmartQuotes {
|
||||
keys = ['i', 'n', '"'];
|
||||
readonly charToMatch = '"';
|
||||
override readonly which = 'next';
|
||||
override includeQuotes = false;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveAroundLastDoubleQuotes extends SmartQuotes {
|
||||
keys = ['a', 'l', '"'];
|
||||
readonly charToMatch = '"';
|
||||
override readonly which = 'last';
|
||||
override includeQuotes = true;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveInsideLastDoubleQuotes extends SmartQuotes {
|
||||
keys = ['i', 'l', '"'];
|
||||
readonly charToMatch = '"';
|
||||
override readonly which = 'last';
|
||||
override includeQuotes = false;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveAroundNextBacktick extends SmartQuotes {
|
||||
keys = ['a', 'n', '`'];
|
||||
readonly charToMatch = '`';
|
||||
override readonly which = 'next';
|
||||
override includeQuotes = true;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveInsideNextBacktick extends SmartQuotes {
|
||||
keys = ['i', 'n', '`'];
|
||||
readonly charToMatch = '`';
|
||||
override readonly which = 'next';
|
||||
override includeQuotes = false;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveAroundLastBacktick extends SmartQuotes {
|
||||
keys = ['a', 'l', '`'];
|
||||
readonly charToMatch = '`';
|
||||
override readonly which = 'last';
|
||||
override includeQuotes = true;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveInsideLastBacktick extends SmartQuotes {
|
||||
keys = ['i', 'l', '`'];
|
||||
readonly charToMatch = '`';
|
||||
override readonly which = 'last';
|
||||
override includeQuotes = false;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveAroundQuote extends SmartQuotes {
|
||||
keys = ['a', 'q'];
|
||||
override readonly anyQuote = true;
|
||||
readonly charToMatch = '"'; // it is not in use, because anyQuote is true.
|
||||
override includeQuotes = true;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveInsideQuote extends SmartQuotes {
|
||||
keys = ['i', 'q'];
|
||||
override readonly anyQuote = true;
|
||||
readonly charToMatch = '"'; // it is not in use, because anyQuote is true.
|
||||
override includeQuotes = false;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveAroundNextQuote extends SmartQuotes {
|
||||
keys = ['a', 'n', 'q'];
|
||||
override readonly which = 'next';
|
||||
override readonly anyQuote = true;
|
||||
readonly charToMatch = '"'; // it is not in use, because anyQuote is true.
|
||||
override includeQuotes = true;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveInsideNextQuote extends SmartQuotes {
|
||||
keys = ['i', 'n', 'q'];
|
||||
override readonly which = 'next';
|
||||
override readonly anyQuote = true;
|
||||
readonly charToMatch = '"'; // it is not in use, because anyQuote is true.
|
||||
override includeQuotes = false;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveAroundLastQuote extends SmartQuotes {
|
||||
keys = ['a', 'l', 'q'];
|
||||
override readonly which = 'last';
|
||||
override readonly anyQuote = true;
|
||||
readonly charToMatch = '"'; // it is not in use, because anyQuote is true.
|
||||
override includeQuotes = true;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class MoveInsideLastQuote extends SmartQuotes {
|
||||
keys = ['i', 'l', 'q'];
|
||||
override readonly which = 'last';
|
||||
override readonly anyQuote = true;
|
||||
readonly charToMatch = '"'; // it is not in use, because anyQuote is true.
|
||||
override includeQuotes = false;
|
||||
}
|
367
src/actions/plugins/targets/smartQuotesMatcher.ts
Normal file
367
src/actions/plugins/targets/smartQuotesMatcher.ts
Normal file
@ -0,0 +1,367 @@
|
||||
import { Position } from 'vscode';
|
||||
import { TextDocument } from 'vscode';
|
||||
import { configuration } from '../../../configuration/configuration';
|
||||
|
||||
type Quote = '"' | "'" | '`';
|
||||
enum QuoteMatch {
|
||||
Opening,
|
||||
Closing,
|
||||
}
|
||||
export type WhichQuotes = 'current' | 'next' | 'last';
|
||||
type Dir = '>' | '<';
|
||||
type SearchAction = {
|
||||
first: Dir;
|
||||
second: Dir;
|
||||
includeCurrent: boolean;
|
||||
};
|
||||
type QuotesAction = {
|
||||
search: SearchAction | undefined;
|
||||
skipToLeft: number; // for last quotes, how many quotes need to skip while searching
|
||||
skipToRight: number; // for next quotes, how many quotes need to skip while searching
|
||||
};
|
||||
|
||||
/**
|
||||
* This mapping is used to give a way to identify which action we need to take when operating on a line.
|
||||
* The keys here are, in some sense, the number of quotes in the line, in the format of `lcr`, where:
|
||||
* `l` means left of the cursor, `c` whether the cursor is on a quote, and `r` is right of the cursor.
|
||||
*
|
||||
* It is based on the ideas used in `targets.vim`. For each line & cursor position, we count the number of quotes
|
||||
* left (#L) and right (#R) of the cursor. Using those numbers and whether the cursor it on a quote, we know
|
||||
* what action to make.
|
||||
*
|
||||
* For each entry we have an example of a line & position.
|
||||
*/
|
||||
const quoteDirs: Record<string, QuotesAction> = {
|
||||
'002': {
|
||||
// | "a" "b" "c"
|
||||
search: { first: '>', second: '>', includeCurrent: false },
|
||||
skipToLeft: 0,
|
||||
skipToRight: 1,
|
||||
},
|
||||
'012': {
|
||||
// |"a" "b" "c" "
|
||||
search: { first: '>', second: '>', includeCurrent: true },
|
||||
skipToLeft: 0,
|
||||
skipToRight: 2,
|
||||
},
|
||||
'102': {
|
||||
// "a" "|b" "c" "
|
||||
search: { first: '<', second: '>', includeCurrent: false },
|
||||
skipToLeft: 2,
|
||||
skipToRight: 2,
|
||||
},
|
||||
'112': {
|
||||
// "a" "b|" "c"
|
||||
search: { first: '<', second: '<', includeCurrent: true },
|
||||
skipToLeft: 2,
|
||||
skipToRight: 1,
|
||||
},
|
||||
'202': {
|
||||
// "a"| "b" "c"
|
||||
search: { first: '>', second: '>', includeCurrent: false },
|
||||
skipToLeft: 1,
|
||||
skipToRight: 1,
|
||||
},
|
||||
'211': {
|
||||
// "a" |"b" "c"
|
||||
search: { first: '>', second: '>', includeCurrent: true },
|
||||
skipToLeft: 1,
|
||||
skipToRight: 2,
|
||||
},
|
||||
'101': {
|
||||
// "a" "|b" "c"
|
||||
search: { first: '<', second: '>', includeCurrent: false },
|
||||
skipToLeft: 2,
|
||||
skipToRight: 2,
|
||||
},
|
||||
'011': {
|
||||
// |"a" "b" "c"
|
||||
search: { first: '>', second: '>', includeCurrent: true },
|
||||
skipToLeft: 0,
|
||||
skipToRight: 2,
|
||||
},
|
||||
'110': {
|
||||
// "a" "b" "c|"
|
||||
search: { first: '<', second: '<', includeCurrent: true },
|
||||
skipToLeft: 2,
|
||||
skipToRight: 0,
|
||||
},
|
||||
'212': {
|
||||
// "a" |"b" "c" "
|
||||
search: { first: '>', second: '>', includeCurrent: true },
|
||||
skipToLeft: 1,
|
||||
skipToRight: 2,
|
||||
},
|
||||
'111': {
|
||||
// "a" "b|" "c" "
|
||||
search: { first: '<', second: '<', includeCurrent: true },
|
||||
skipToLeft: 2,
|
||||
skipToRight: 1,
|
||||
},
|
||||
'200': {
|
||||
// "a" "b" "c"|
|
||||
search: { first: '<', second: '<', includeCurrent: false },
|
||||
skipToLeft: 1,
|
||||
skipToRight: 0,
|
||||
},
|
||||
'201': {
|
||||
// "a" "b" "c"| "
|
||||
// "a"| "b" "c" "
|
||||
search: { first: '>', second: '>', includeCurrent: false },
|
||||
skipToLeft: 1,
|
||||
skipToRight: 1,
|
||||
},
|
||||
'210': {
|
||||
// "a" "b" "c" |"
|
||||
search: undefined,
|
||||
skipToLeft: 1,
|
||||
skipToRight: 0,
|
||||
},
|
||||
'001': {
|
||||
// | "a" "b" "c" "
|
||||
search: undefined,
|
||||
skipToLeft: 0,
|
||||
skipToRight: 1,
|
||||
},
|
||||
'010': {
|
||||
// a|"b
|
||||
search: undefined,
|
||||
skipToLeft: 0,
|
||||
skipToRight: 0,
|
||||
},
|
||||
'100': {
|
||||
// "a" "b" "c" "|
|
||||
search: undefined,
|
||||
skipToLeft: 2,
|
||||
skipToRight: 0,
|
||||
},
|
||||
'000': {
|
||||
// |ab
|
||||
search: undefined,
|
||||
skipToLeft: 0,
|
||||
skipToRight: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export class SmartQuoteMatcher {
|
||||
static readonly escapeChar = '\\';
|
||||
|
||||
private document: TextDocument;
|
||||
private quote: Quote | 'any';
|
||||
|
||||
constructor(quote: Quote | 'any', document: TextDocument) {
|
||||
this.quote = quote;
|
||||
this.document = document;
|
||||
}
|
||||
|
||||
private buildQuoteMap(text: string) {
|
||||
const quoteMap: QuoteMatch[] = [];
|
||||
let openingQuote = true;
|
||||
// Loop over text, marking quotes and respecting escape characters.
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text[i] === SmartQuoteMatcher.escapeChar) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
(this.quote === 'any' && (text[i] === '"' || text[i] === "'" || text[i] === '`')) ||
|
||||
text[i] === this.quote
|
||||
) {
|
||||
quoteMap[i] = openingQuote ? QuoteMatch.Opening : QuoteMatch.Closing;
|
||||
openingQuote = !openingQuote;
|
||||
}
|
||||
}
|
||||
return quoteMap;
|
||||
}
|
||||
|
||||
private static lineSearchAction(cursorIndex: number, quoteMap: QuoteMatch[]) {
|
||||
// base on ideas from targets.vim
|
||||
|
||||
// cut line in left of, on and right of cursor
|
||||
const left = Array.from(quoteMap.entries()).slice(undefined, cursorIndex);
|
||||
const cursor = quoteMap[cursorIndex];
|
||||
const right = Array.from(quoteMap.entries()).slice(cursorIndex + 1, undefined);
|
||||
|
||||
// how many delimiters left, on and right of cursor
|
||||
const lc = left.filter(([_, v]) => v !== undefined).length;
|
||||
const cc = cursor !== undefined ? 1 : 0;
|
||||
const rc = right.filter(([_, v]) => v !== undefined).length;
|
||||
|
||||
// truncate counts
|
||||
const lct = lc === 0 ? 0 : lc % 2 === 0 ? 2 : 1;
|
||||
const rct = rc === 0 ? 0 : rc >= 2 ? 2 : 1;
|
||||
|
||||
const key = `${lct}${cc}${rct}`;
|
||||
const act = quoteDirs[key];
|
||||
|
||||
return act;
|
||||
}
|
||||
|
||||
public smartSurroundingQuotes(
|
||||
position: Position,
|
||||
which: WhichQuotes
|
||||
): { start: Position; stop: Position; lineText: string } | undefined {
|
||||
position = this.document.validatePosition(position);
|
||||
const cursorIndex = position.character;
|
||||
const lineText = this.document.lineAt(position).text;
|
||||
const quoteMap = this.buildQuoteMap(lineText);
|
||||
|
||||
const act = SmartQuoteMatcher.lineSearchAction(cursorIndex, quoteMap);
|
||||
|
||||
if (which === 'current') {
|
||||
if (act.search) {
|
||||
const searchRes = this.smartSearch(cursorIndex, act.search, quoteMap);
|
||||
return searchRes
|
||||
? {
|
||||
start: position.with({ character: searchRes[0] }),
|
||||
stop: position.with({ character: searchRes[1] }),
|
||||
lineText,
|
||||
}
|
||||
: undefined;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else if (which === 'next') {
|
||||
// search quote in current line
|
||||
const right = Array.from(quoteMap.entries()).slice(cursorIndex + 1, undefined);
|
||||
const [index, found] = right.filter(([i, v]) => v !== undefined)[act.skipToRight] ?? [
|
||||
+Infinity,
|
||||
undefined,
|
||||
];
|
||||
// find next position for surrounding quotes, possibly breaking through lines
|
||||
let nextPos;
|
||||
position = position.with({ character: index });
|
||||
if (found === undefined && configuration.targets.smartQuotes.breakThroughLines) {
|
||||
// nextPos = State.evalGenerator(this.getNextQuoteThroughLineBreaks(), position);
|
||||
nextPos = this.getNextQuoteThroughLineBreaks(position);
|
||||
} else {
|
||||
nextPos = found !== undefined ? position : undefined;
|
||||
}
|
||||
|
||||
// find surrounding with new position
|
||||
if (nextPos) {
|
||||
return this.smartSurroundingQuotes(nextPos, 'current');
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else if (which === 'last') {
|
||||
// search quote in current line
|
||||
const left = Array.from(quoteMap.entries()).slice(undefined, cursorIndex);
|
||||
const [index, found] = left.reverse().filter(([i, v]) => v !== undefined)[act.skipToLeft] ?? [
|
||||
0,
|
||||
undefined,
|
||||
];
|
||||
// find last position for surrounding quotes, possibly breaking through lines
|
||||
let lastPos;
|
||||
position = position.with({ character: index });
|
||||
if (found === undefined && configuration.targets.smartQuotes.breakThroughLines) {
|
||||
position = position.getLeftThroughLineBreaks();
|
||||
lastPos = this.getLastQuoteThroughLineBreaks(position);
|
||||
} else {
|
||||
lastPos = found !== undefined ? position : undefined;
|
||||
}
|
||||
|
||||
// find surrounding with new position
|
||||
if (lastPos) {
|
||||
return this.smartSurroundingQuotes(lastPos, 'current');
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private smartSearch(
|
||||
start: number,
|
||||
action: SearchAction,
|
||||
quoteMap: QuoteMatch[]
|
||||
): [number, number] | undefined {
|
||||
const offset = action.includeCurrent ? 1 : 0;
|
||||
let cursorPos: number | undefined = start;
|
||||
let fst: number | undefined;
|
||||
let snd: number | undefined;
|
||||
|
||||
if (action.first === '>') {
|
||||
cursorPos = fst = this.getNextQuote(cursorPos - offset, quoteMap);
|
||||
} else {
|
||||
// dir === '<'
|
||||
cursorPos = fst = this.getPrevQuote(cursorPos + offset, quoteMap);
|
||||
}
|
||||
if (cursorPos === undefined) return undefined;
|
||||
|
||||
if (action.second === '>') {
|
||||
snd = this.getNextQuote(cursorPos, quoteMap);
|
||||
} else {
|
||||
// dir === '<'
|
||||
snd = this.getPrevQuote(cursorPos, quoteMap);
|
||||
}
|
||||
|
||||
if (fst === undefined || snd === undefined) return undefined;
|
||||
|
||||
if (fst < snd) return [fst, snd];
|
||||
else return [snd, fst];
|
||||
}
|
||||
|
||||
private getNextQuoteThroughLineBreaks(position: Position): Position | undefined {
|
||||
for (let line = position.line; line < this.document.lineCount; line++) {
|
||||
position = this.document.validatePosition(position.with({ line }));
|
||||
const text = this.document.lineAt(position).text;
|
||||
if (this.quote === 'any') {
|
||||
for (let i = position.character; i < text.length; i++) {
|
||||
if (text[i] === '"' || text[i] === "'" || text[i] === '`') {
|
||||
return position.with({ character: i });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const index = text.indexOf(this.quote, position.character);
|
||||
if (index >= 0) {
|
||||
return position.with({ character: index });
|
||||
}
|
||||
}
|
||||
position = position.with({ character: 0 }); // set at line begin for next iteration
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
private getLastQuoteThroughLineBreaks(position: Position): Position | undefined {
|
||||
for (let line = position.line; line >= 0; line--) {
|
||||
position = this.document.validatePosition(position.with({ line }));
|
||||
const text = this.document.lineAt(position).text;
|
||||
if (this.quote === 'any') {
|
||||
for (let i = position.character; i >= 0; i--) {
|
||||
if (text[i] === '"' || text[i] === "'" || text[i] === '`') {
|
||||
return position.with({ character: i });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const index = text.lastIndexOf(this.quote, position.character);
|
||||
if (index >= 0) {
|
||||
return position.with({ character: index });
|
||||
}
|
||||
}
|
||||
position = position.with({ character: +Infinity }); // set at line end for next iteration
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getNextQuote(start: number, quoteMap: QuoteMatch[]): number | undefined {
|
||||
for (let i = start + 1; i < quoteMap.length; i++) {
|
||||
if (quoteMap[i] !== undefined) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getPrevQuote(start: number, quoteMap: QuoteMatch[]): number | undefined {
|
||||
for (let i = start - 1; i >= 0; i--) {
|
||||
if (quoteMap[i] !== undefined) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
2
src/actions/plugins/targets/targets.ts
Normal file
2
src/actions/plugins/targets/targets.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// targets sub-plugins
|
||||
import './smartQuotes';
|
9
src/actions/plugins/targets/targetsConfig.ts
Normal file
9
src/actions/plugins/targets/targetsConfig.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { configuration } from '../../../configuration/configuration';
|
||||
|
||||
export function useSmartQuotes(): boolean {
|
||||
return (
|
||||
(configuration.targets.enable === true && configuration.targets.smartQuotes.enable !== false) ||
|
||||
(configuration.targets.enable === undefined &&
|
||||
configuration.targets.smartQuotes.enable === true)
|
||||
);
|
||||
}
|
@ -15,6 +15,7 @@ import {
|
||||
IDebugConfiguration,
|
||||
IHighlightedYankConfiguration,
|
||||
ICamelCaseMotionConfiguration,
|
||||
ITargetsConfiguration,
|
||||
} from './iconfiguration';
|
||||
|
||||
import * as packagejson from '../../package.json';
|
||||
@ -267,6 +268,16 @@ class Configuration implements IConfiguration {
|
||||
easymotionKeys = 'hklyuiopnm,qwertzxcvbasdgjf;';
|
||||
easymotionJumpToAnywhereRegex = '\\b[A-Za-z0-9]|[A-Za-z0-9]\\b|_.|#.|[a-z][A-Z]';
|
||||
|
||||
targets: ITargetsConfiguration = {
|
||||
enable: false,
|
||||
|
||||
smartQuotes: {
|
||||
enable: false,
|
||||
breakThroughLines: false,
|
||||
aIncludesSurroundingSpaces: true,
|
||||
},
|
||||
};
|
||||
|
||||
autoSwitchInputMethod: IAutoSwitchInputMethod = {
|
||||
enable: false,
|
||||
defaultIM: '',
|
||||
|
@ -80,6 +80,29 @@ export interface ICamelCaseMotionConfiguration {
|
||||
enable: boolean;
|
||||
}
|
||||
|
||||
export interface ISmartQuotesConfiguration {
|
||||
/**
|
||||
* Enable SmartQuotes plugin or not
|
||||
*/
|
||||
enable: boolean;
|
||||
/**
|
||||
* Whether to break through lines when using [n]ext/[l]ast motion
|
||||
*/
|
||||
breakThroughLines: boolean;
|
||||
/**
|
||||
* Whether to use default vim behaviour when using `a` (e.g. da') which include surrounding spaces, or not, as for other text objects.
|
||||
*/
|
||||
aIncludesSurroundingSpaces: boolean;
|
||||
}
|
||||
|
||||
export interface ITargetsConfiguration {
|
||||
/**
|
||||
* Enable Targets plugin or not
|
||||
*/
|
||||
enable: boolean;
|
||||
smartQuotes: ISmartQuotesConfiguration;
|
||||
}
|
||||
|
||||
export interface IConfiguration {
|
||||
/**
|
||||
* Use the system's clipboard when copying.
|
||||
|
1013
test/plugins/smartQuotes.test.ts
Normal file
1013
test/plugins/smartQuotes.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@ import {
|
||||
IHighlightedYankConfiguration,
|
||||
IKeyRemapping,
|
||||
IModeSpecificStrings,
|
||||
ITargetsConfiguration,
|
||||
} from '../src/configuration/iconfiguration';
|
||||
|
||||
export class Configuration implements IConfiguration {
|
||||
@ -45,6 +46,14 @@ export class Configuration implements IConfiguration {
|
||||
easymotionMarkerFontWeight = 'bold';
|
||||
easymotionMarkerMargin = 0; // Deprecated! No longer needed!
|
||||
easymotionKeys = 'hklyuiopnm,qwertzxcvbasdgjf;';
|
||||
targets: ITargetsConfiguration = {
|
||||
enable: false,
|
||||
smartQuotes: {
|
||||
enable: false,
|
||||
breakThroughLines: true,
|
||||
aIncludesSurroundingSpaces: true,
|
||||
},
|
||||
};
|
||||
autoSwitchInputMethod = {
|
||||
enable: false,
|
||||
defaultIM: '',
|
||||
|
Loading…
Reference in New Issue
Block a user