mirror of
https://github.com/jasonwilliams/vscode-helix.git
synced 2024-09-11 16:56:32 +03:00
initial fork from modern VSCode/Vim
This commit is contained in:
parent
ad779c6282
commit
2d190685d5
20
build.mjs
20
build.mjs
@ -1,11 +1,15 @@
|
||||
import esbuild from 'esbuild';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
const production = process.argv[2] === '--production';
|
||||
const watch = process.argv[2] === '--watch';
|
||||
let desktopContext, browserContext;
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// This is the base config that will be used by both web and desktop versions of the extension
|
||||
const baseConfig = {
|
||||
entryPoints: ['./src/index.ts'],
|
||||
entryPoints: ['./extension.ts'],
|
||||
bundle: true,
|
||||
external: ['vscode'],
|
||||
sourcemap: !production,
|
||||
@ -19,7 +23,19 @@ try {
|
||||
// https://esbuild.github.io/getting-started/#bundling-for-node
|
||||
esbuild.context({ ...baseConfig, outfile: './dist/index.js', platform: 'node' }),
|
||||
// https://esbuild.github.io/getting-started/#bundling-for-the-browser
|
||||
esbuild.context({ ...baseConfig, outfile: './dist/browser.js', platform: 'browser' }),
|
||||
esbuild.context({
|
||||
...baseConfig,
|
||||
external: ['child_process', 'vscode'],
|
||||
alias: {
|
||||
os: 'os-browserify/browser',
|
||||
path: 'path-browserify',
|
||||
process: 'process/browser',
|
||||
util: 'util',
|
||||
platform: path.resolve(__dirname, 'src', 'platform', 'browser'),
|
||||
},
|
||||
outfile: './dist/browser.js',
|
||||
platform: 'browser',
|
||||
}),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
57
extension.ts
Normal file
57
extension.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Extension.ts is a lightweight wrapper around ModeHandler. It converts key
|
||||
* events to their string names and passes them on to ModeHandler via
|
||||
* handleKeyEvent().
|
||||
*/
|
||||
import './src/actions/include-main';
|
||||
import './src/actions/include-plugins';
|
||||
|
||||
/**
|
||||
* Load configuration validator
|
||||
*/
|
||||
|
||||
import './src/configuration/validators/inputMethodSwitcherValidator';
|
||||
import './src/configuration/validators/remappingValidator';
|
||||
import './src/configuration/validators/vimrcValidator';
|
||||
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { activate as activateFunc, loadConfiguration, registerCommand, registerEventListener } from './extensionBase';
|
||||
import { vimrc } from './src/configuration/vimrc';
|
||||
import { Globals } from './src/globals';
|
||||
import { Register } from './src/register/register';
|
||||
import { Logger } from './src/util/logger';
|
||||
|
||||
export { getAndUpdateModeHandler } from './extensionBase';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
// Set the storage path to be used by history files
|
||||
Globals.extensionStoragePath = context.globalStorageUri.fsPath;
|
||||
|
||||
await activateFunc(context);
|
||||
|
||||
registerEventListener(context, vscode.workspace.onDidSaveTextDocument, async (document) => {
|
||||
if (vimrc.vimrcPath && path.relative(document.fileName, vimrc.vimrcPath) === '') {
|
||||
await loadConfiguration();
|
||||
Logger.info('Sourced new .vimrc');
|
||||
}
|
||||
});
|
||||
|
||||
registerCommand(
|
||||
context,
|
||||
'vim.editVimrc',
|
||||
async () => {
|
||||
if (vimrc.vimrcPath) {
|
||||
const document = await vscode.workspace.openTextDocument(vimrc.vimrcPath);
|
||||
await vscode.window.showTextDocument(document);
|
||||
} else {
|
||||
await vscode.window.showWarningMessage('No .vimrc found. Please set `vim.vimrc.path`.');
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
export async function deactivate() {
|
||||
await Register.saveToDisk(true);
|
||||
}
|
638
extensionBase.ts
Normal file
638
extensionBase.ts
Normal file
@ -0,0 +1,638 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { ExCommandLine, SearchCommandLine } from './src/cmd_line/commandLine';
|
||||
import { configuration } from './src/configuration/configuration';
|
||||
import { Notation } from './src/configuration/notation';
|
||||
import { Globals } from './src/globals';
|
||||
import { Jump } from './src/jumps/jump';
|
||||
import { Mode } from './src/mode/mode';
|
||||
import { ModeHandler } from './src/mode/modeHandler';
|
||||
import { ModeHandlerMap } from './src/mode/modeHandlerMap';
|
||||
import { Register } from './src/register/register';
|
||||
import { CompositionState } from './src/state/compositionState';
|
||||
import { globalState } from './src/state/globalState';
|
||||
import { StatusBar } from './src/statusBar';
|
||||
import { taskQueue } from './src/taskQueue';
|
||||
import { Logger } from './src/util/logger';
|
||||
import { SpecialKeys } from './src/util/specialKeys';
|
||||
import { VSCodeContext } from './src/util/vscodeContext';
|
||||
import { exCommandParser } from './src/vimscript/exCommandParser';
|
||||
|
||||
let extensionContext: vscode.ExtensionContext;
|
||||
let previousActiveEditorUri: vscode.Uri | undefined;
|
||||
let lastClosedModeHandler: ModeHandler | null = null;
|
||||
|
||||
interface ICodeKeybinding {
|
||||
after?: string[];
|
||||
commands?: Array<{ command: string; args: any[] }>;
|
||||
}
|
||||
|
||||
export async function getAndUpdateModeHandler(
|
||||
forceSyncAndUpdate = false,
|
||||
): Promise<ModeHandler | undefined> {
|
||||
const activeTextEditor = vscode.window.activeTextEditor;
|
||||
if (activeTextEditor === undefined || activeTextEditor.document.isClosed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [curHandler, isNew] = await ModeHandlerMap.getOrCreate(activeTextEditor);
|
||||
if (isNew) {
|
||||
extensionContext.subscriptions.push(curHandler);
|
||||
}
|
||||
|
||||
curHandler.vimState.editor = activeTextEditor;
|
||||
|
||||
if (
|
||||
forceSyncAndUpdate ||
|
||||
!previousActiveEditorUri ||
|
||||
previousActiveEditorUri !== activeTextEditor.document.uri
|
||||
) {
|
||||
// We sync the cursors here because ModeHandler is specific to a document, not an editor, so we
|
||||
// need to update our representation of the cursors when switching between editors for the same document.
|
||||
// This will be unnecessary once #4889 is fixed.
|
||||
curHandler.syncCursors();
|
||||
await curHandler.updateView({ drawSelection: false, revealRange: false });
|
||||
}
|
||||
|
||||
previousActiveEditorUri = activeTextEditor.document.uri;
|
||||
|
||||
if (curHandler.focusChanged) {
|
||||
curHandler.focusChanged = false;
|
||||
|
||||
if (previousActiveEditorUri) {
|
||||
const prevHandler = ModeHandlerMap.get(previousActiveEditorUri);
|
||||
prevHandler!.focusChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
return curHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and validates the user's configuration
|
||||
*/
|
||||
export async function loadConfiguration() {
|
||||
const validatorResults = await configuration.load();
|
||||
|
||||
Logger.debug(`${validatorResults.numErrors} errors found with vim configuration`);
|
||||
|
||||
if (validatorResults.numErrors > 0) {
|
||||
for (const validatorResult of validatorResults.get()) {
|
||||
switch (validatorResult.level) {
|
||||
case 'error':
|
||||
Logger.error(validatorResult.message);
|
||||
break;
|
||||
case 'warning':
|
||||
Logger.warn(validatorResult.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The extension's entry point
|
||||
*/
|
||||
export async function activate(context: vscode.ExtensionContext, handleLocal: boolean = true) {
|
||||
ExCommandLine.parser = exCommandParser;
|
||||
|
||||
Logger.init();
|
||||
|
||||
// before we do anything else, we need to load the configuration
|
||||
await loadConfiguration();
|
||||
|
||||
Logger.debug('Start');
|
||||
|
||||
extensionContext = context;
|
||||
extensionContext.subscriptions.push(StatusBar);
|
||||
|
||||
// Load state
|
||||
Register.loadFromDisk(handleLocal);
|
||||
await Promise.all([ExCommandLine.loadHistory(context), SearchCommandLine.loadHistory(context)]);
|
||||
|
||||
if (vscode.window.activeTextEditor) {
|
||||
const filepathComponents = vscode.window.activeTextEditor.document.fileName.split(/\\|\//);
|
||||
Register.setReadonlyRegister('%', filepathComponents[filepathComponents.length - 1]);
|
||||
}
|
||||
|
||||
// workspace events
|
||||
registerEventListener(
|
||||
context,
|
||||
vscode.workspace.onDidChangeConfiguration,
|
||||
async () => {
|
||||
Logger.info('Configuration changed');
|
||||
await loadConfiguration();
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
registerEventListener(context, vscode.workspace.onDidChangeTextDocument, async (event) => {
|
||||
if (event.document.uri.scheme === 'output') {
|
||||
// Without this, we'll get an infinite logging loop
|
||||
return;
|
||||
}
|
||||
if (event.contentChanges.length === 0) {
|
||||
// This happens when the document is saved
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.debug(
|
||||
`${event.contentChanges.length} change(s) to ${event.document.fileName} because ${event.reason}`,
|
||||
);
|
||||
for (const x of event.contentChanges) {
|
||||
Logger.trace(`\t-${x.rangeLength}, +'${x.text}'`);
|
||||
}
|
||||
|
||||
if (event.contentChanges.length === 1) {
|
||||
const change = event.contentChanges[0];
|
||||
|
||||
const anyLinesDeleted = change.range.start.line !== change.range.end.line;
|
||||
|
||||
if (anyLinesDeleted && change.text === '') {
|
||||
globalState.jumpTracker.handleTextDeleted(event.document, change.range);
|
||||
} else if (!anyLinesDeleted && change.text.includes('\n')) {
|
||||
globalState.jumpTracker.handleTextAdded(event.document, change.range, change.text);
|
||||
} else {
|
||||
// TODO: What to do here?
|
||||
}
|
||||
} else {
|
||||
// TODO: In this case, we should probably loop over the content changes...
|
||||
}
|
||||
|
||||
// Change from VSCode editor should set document.isDirty to true but they initially don't!
|
||||
// There is a timing issue in VSCode codebase between when the isDirty flag is set and
|
||||
// when registered callbacks are fired. https://github.com/Microsoft/vscode/issues/11339
|
||||
const contentChangeHandler = (modeHandler: ModeHandler) => {
|
||||
if (modeHandler.vimState.currentMode === Mode.Insert) {
|
||||
if (modeHandler.vimState.historyTracker.currentContentChanges === undefined) {
|
||||
modeHandler.vimState.historyTracker.currentContentChanges = [];
|
||||
}
|
||||
|
||||
modeHandler.vimState.historyTracker.currentContentChanges =
|
||||
modeHandler.vimState.historyTracker.currentContentChanges.concat(event.contentChanges);
|
||||
}
|
||||
};
|
||||
|
||||
const mh = ModeHandlerMap.get(event.document.uri);
|
||||
if (mh) {
|
||||
contentChangeHandler(mh);
|
||||
}
|
||||
});
|
||||
|
||||
registerEventListener(
|
||||
context,
|
||||
vscode.workspace.onDidCloseTextDocument,
|
||||
async (closedDocument) => {
|
||||
Logger.info(`${closedDocument.fileName} closed`);
|
||||
|
||||
// Delete modehandler once all tabs of this document have been closed
|
||||
for (const [uri, modeHandler] of ModeHandlerMap.entries()) {
|
||||
let shouldDelete = false;
|
||||
if (modeHandler == null) {
|
||||
shouldDelete = true;
|
||||
} else {
|
||||
const document = modeHandler.vimState.document;
|
||||
if (!vscode.workspace.textDocuments.includes(document)) {
|
||||
shouldDelete = true;
|
||||
if (closedDocument === document) {
|
||||
lastClosedModeHandler = modeHandler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDelete) {
|
||||
ModeHandlerMap.delete(uri);
|
||||
}
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
// window events
|
||||
registerEventListener(
|
||||
context,
|
||||
vscode.window.onDidChangeActiveTextEditor,
|
||||
async (activeTextEditor: vscode.TextEditor | undefined) => {
|
||||
if (activeTextEditor) {
|
||||
Logger.info(`Active editor: ${activeTextEditor.document.uri}`);
|
||||
} else {
|
||||
Logger.debug(`No active editor`);
|
||||
}
|
||||
|
||||
const mhPrevious: ModeHandler | undefined = previousActiveEditorUri
|
||||
? ModeHandlerMap.get(previousActiveEditorUri)
|
||||
: undefined;
|
||||
// Track the closed editor so we can use it the next time an open event occurs.
|
||||
// When vscode changes away from a temporary file, onDidChangeActiveTextEditor first twice.
|
||||
// First it fires when leaving the closed editor. Then onDidCloseTextDocument first, and we delete
|
||||
// the old ModeHandler. Then a new editor opens.
|
||||
//
|
||||
// This also applies to files that are merely closed, which allows you to jump back to that file similarly
|
||||
// once a new file is opened.
|
||||
lastClosedModeHandler = mhPrevious || lastClosedModeHandler;
|
||||
|
||||
const oldFileRegister = (await Register.get('%'))?.text;
|
||||
const relativePath = activeTextEditor
|
||||
? vscode.workspace.asRelativePath(activeTextEditor.document.uri, false)
|
||||
: '';
|
||||
|
||||
if (relativePath !== oldFileRegister) {
|
||||
if (oldFileRegister && oldFileRegister !== '') {
|
||||
Register.setReadonlyRegister('#', oldFileRegister as string);
|
||||
}
|
||||
Register.setReadonlyRegister('%', relativePath);
|
||||
}
|
||||
|
||||
if (activeTextEditor === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
taskQueue.enqueueTask(async () => {
|
||||
const mh = await getAndUpdateModeHandler(true);
|
||||
if (mh) {
|
||||
globalState.jumpTracker.handleFileJump(
|
||||
lastClosedModeHandler ? Jump.fromStateNow(lastClosedModeHandler.vimState) : null,
|
||||
Jump.fromStateNow(mh.vimState),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
registerEventListener(
|
||||
context,
|
||||
vscode.window.onDidChangeTextEditorSelection,
|
||||
async (e: vscode.TextEditorSelectionChangeEvent) => {
|
||||
if (e.textEditor.document.uri.scheme === 'output') {
|
||||
// Without this, we can an infinite logging loop
|
||||
return;
|
||||
}
|
||||
if (
|
||||
vscode.window.activeTextEditor === undefined ||
|
||||
e.textEditor.document !== vscode.window.activeTextEditor.document
|
||||
) {
|
||||
// We don't care if user selection changed in a paneled window (e.g debug console/terminal)
|
||||
return;
|
||||
}
|
||||
|
||||
const mh = ModeHandlerMap.get(vscode.window.activeTextEditor.document.uri);
|
||||
if (mh === undefined) {
|
||||
// We don't care if there is no active editor
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.kind !== vscode.TextEditorSelectionChangeKind.Mouse) {
|
||||
const selectionsHash = e.selections.reduce(
|
||||
(hash, s) =>
|
||||
hash +
|
||||
`[${s.anchor.line}, ${s.anchor.character}; ${s.active.line}, ${s.active.character}]`,
|
||||
'',
|
||||
);
|
||||
const idx = mh.selectionsChanged.ourSelections.indexOf(selectionsHash);
|
||||
if (idx > -1) {
|
||||
mh.selectionsChanged.ourSelections.splice(idx, 1);
|
||||
Logger.trace(
|
||||
`Ignoring selection: ${selectionsHash}. ${mh.selectionsChanged.ourSelections.length} left`,
|
||||
);
|
||||
return;
|
||||
} else if (mh.selectionsChanged.ignoreIntermediateSelections) {
|
||||
Logger.trace(`Ignoring intermediate selection change: ${selectionsHash}`);
|
||||
return;
|
||||
} else if (mh.selectionsChanged.ourSelections.length > 0) {
|
||||
// Some intermediate selection must have slipped in after setting the
|
||||
// 'ignoreIntermediateSelections' to false. Which means we didn't count
|
||||
// for it yet, but since we have selections to be ignored then we probably
|
||||
// wanted this one to be ignored as well.
|
||||
Logger.warn(`Ignoring slipped selection: ${selectionsHash}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We may receive changes from other panels when, having selections in them containing the same file
|
||||
// and changing text before the selection in current panel.
|
||||
if (e.textEditor !== mh.vimState.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mh.focusChanged) {
|
||||
mh.focusChanged = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mh.currentMode === Mode.EasyMotionMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
taskQueue.enqueueTask(() => mh.handleSelectionChange(e));
|
||||
},
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
registerEventListener(
|
||||
context,
|
||||
vscode.window.onDidChangeTextEditorVisibleRanges,
|
||||
async (e: vscode.TextEditorVisibleRangesChangeEvent) => {
|
||||
if (e.textEditor !== vscode.window.activeTextEditor) {
|
||||
return;
|
||||
}
|
||||
taskQueue.enqueueTask(async () => {
|
||||
// Scrolling the viewport clears any status bar message, even errors.
|
||||
const mh = await getAndUpdateModeHandler();
|
||||
if (mh && StatusBar.lastMessageTime) {
|
||||
// TODO: Using the time elapsed works most of the time, but is a bit of a hack
|
||||
const timeElapsed = Date.now() - Number(StatusBar.lastMessageTime);
|
||||
if (timeElapsed > 100) {
|
||||
StatusBar.clear(mh.vimState, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const compositionState = new CompositionState();
|
||||
|
||||
// Override VSCode commands
|
||||
overrideCommand(context, 'type', async (args: { text: string }) => {
|
||||
taskQueue.enqueueTask(async () => {
|
||||
const mh = await getAndUpdateModeHandler();
|
||||
if (mh) {
|
||||
if (compositionState.isInComposition) {
|
||||
compositionState.composingText += args.text;
|
||||
if (mh.vimState.currentMode === Mode.Insert) {
|
||||
compositionState.insertedText = true;
|
||||
void vscode.commands.executeCommand('default:type', { text: args.text });
|
||||
}
|
||||
} else {
|
||||
await mh.handleKeyEvent(args.text);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
overrideCommand(
|
||||
context,
|
||||
'replacePreviousChar',
|
||||
async (args: { replaceCharCnt: number; text: string }) => {
|
||||
taskQueue.enqueueTask(async () => {
|
||||
const mh = await getAndUpdateModeHandler();
|
||||
if (mh) {
|
||||
if (compositionState.isInComposition) {
|
||||
compositionState.composingText =
|
||||
compositionState.composingText.substr(
|
||||
0,
|
||||
compositionState.composingText.length - args.replaceCharCnt,
|
||||
) + args.text;
|
||||
}
|
||||
if (compositionState.insertedText) {
|
||||
await vscode.commands.executeCommand('default:replacePreviousChar', {
|
||||
text: args.text,
|
||||
replaceCharCnt: args.replaceCharCnt,
|
||||
});
|
||||
mh.vimState.cursorStopPosition = mh.vimState.editor.selection.start;
|
||||
mh.vimState.cursorStartPosition = mh.vimState.editor.selection.start;
|
||||
}
|
||||
} else {
|
||||
await vscode.commands.executeCommand('default:replacePreviousChar', {
|
||||
text: args.text,
|
||||
replaceCharCnt: args.replaceCharCnt,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
overrideCommand(context, 'compositionStart', async () => {
|
||||
taskQueue.enqueueTask(async () => {
|
||||
compositionState.isInComposition = true;
|
||||
});
|
||||
});
|
||||
|
||||
overrideCommand(context, 'compositionEnd', async () => {
|
||||
taskQueue.enqueueTask(async () => {
|
||||
const mh = await getAndUpdateModeHandler();
|
||||
if (mh) {
|
||||
if (compositionState.insertedText) {
|
||||
mh.selectionsChanged.ignoreIntermediateSelections = true;
|
||||
await vscode.commands.executeCommand('default:replacePreviousChar', {
|
||||
text: '',
|
||||
replaceCharCnt: compositionState.composingText.length,
|
||||
});
|
||||
mh.vimState.cursorStopPosition = mh.vimState.editor.selection.active;
|
||||
mh.vimState.cursorStartPosition = mh.vimState.editor.selection.active;
|
||||
mh.selectionsChanged.ignoreIntermediateSelections = false;
|
||||
}
|
||||
const text = compositionState.composingText;
|
||||
await mh.handleMultipleKeyEvents(text.split(''));
|
||||
}
|
||||
compositionState.reset();
|
||||
});
|
||||
});
|
||||
|
||||
// Register extension commands
|
||||
registerCommand(context, 'vim.showQuickpickCmdLine', async () => {
|
||||
const mh = await getAndUpdateModeHandler();
|
||||
if (mh) {
|
||||
const cmd = await vscode.window.showInputBox({
|
||||
prompt: 'Vim command line',
|
||||
value: '',
|
||||
ignoreFocusOut: false,
|
||||
valueSelection: [0, 0],
|
||||
});
|
||||
if (cmd) {
|
||||
await new ExCommandLine(cmd, mh.vimState.currentMode).run(mh.vimState);
|
||||
}
|
||||
void mh.updateView();
|
||||
}
|
||||
});
|
||||
|
||||
registerCommand(context, 'vim.remap', async (args: ICodeKeybinding) => {
|
||||
taskQueue.enqueueTask(async () => {
|
||||
const mh = await getAndUpdateModeHandler();
|
||||
if (mh === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args) {
|
||||
throw new Error(
|
||||
"'args' is undefined. For this remap to work it needs to have 'args' with an '\"after\": string[]' and/or a '\"commands\": { command: string; args: any[] }[]'",
|
||||
);
|
||||
}
|
||||
|
||||
if (args.after) {
|
||||
for (const key of args.after) {
|
||||
await mh.handleKeyEvent(Notation.NormalizeKey(key, configuration.leader));
|
||||
}
|
||||
}
|
||||
|
||||
if (args.commands) {
|
||||
for (const command of args.commands) {
|
||||
// Check if this is a vim command by looking for :
|
||||
if (command.command.startsWith(':')) {
|
||||
await new ExCommandLine(
|
||||
command.command.slice(1, command.command.length),
|
||||
mh.vimState.currentMode,
|
||||
).run(mh.vimState);
|
||||
void mh.updateView();
|
||||
} else {
|
||||
await vscode.commands.executeCommand(command.command, command.args);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
registerCommand(context, 'toggleVim', async () => {
|
||||
configuration.disableExtension = !configuration.disableExtension;
|
||||
void toggleExtension(configuration.disableExtension, compositionState);
|
||||
});
|
||||
|
||||
for (const boundKey of configuration.boundKeyCombinations) {
|
||||
const command = ['<Esc>', '<C-c>'].includes(boundKey.key)
|
||||
? async () => {
|
||||
const mh = await getAndUpdateModeHandler();
|
||||
if (mh && !(await forceStopRecursiveRemap(mh))) {
|
||||
await mh.handleKeyEvent(`${boundKey.key}`);
|
||||
}
|
||||
}
|
||||
: async () => {
|
||||
const mh = await getAndUpdateModeHandler();
|
||||
if (mh) {
|
||||
await mh.handleKeyEvent(`${boundKey.key}`);
|
||||
}
|
||||
};
|
||||
registerCommand(context, boundKey.command, async () => {
|
||||
taskQueue.enqueueTask(command);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// Initialize mode handler for current active Text Editor at startup.
|
||||
const modeHandler = await getAndUpdateModeHandler();
|
||||
if (modeHandler) {
|
||||
if (!configuration.startInInsertMode) {
|
||||
const vimState = modeHandler.vimState;
|
||||
|
||||
// Make sure no cursors start on the EOL character (which is invalid in normal mode)
|
||||
// This can happen if we quit last session in insert mode at the end of the line
|
||||
vimState.cursors = vimState.cursors.map((cursor) => {
|
||||
const eolColumn = vimState.document.lineAt(cursor.stop).text.length;
|
||||
if (cursor.stop.character >= eolColumn) {
|
||||
const character = Math.max(eolColumn - 1, 0);
|
||||
return cursor.withNewStop(cursor.stop.with({ character }));
|
||||
} else {
|
||||
return cursor;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// This is called last because getAndUpdateModeHandler() will change cursor
|
||||
void modeHandler.updateView({ drawSelection: true, revealRange: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Disable automatic keyboard navigation in lists, so it doesn't interfere
|
||||
// with our list navigation keybindings
|
||||
await VSCodeContext.set('listAutomaticKeyboardNavigation', false);
|
||||
|
||||
await toggleExtension(configuration.disableExtension, compositionState);
|
||||
|
||||
Logger.debug('Finish.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the VSCodeVim extension between Enabled mode and Disabled mode. This
|
||||
* function is activated by calling the 'toggleVim' command from the Command Palette.
|
||||
*
|
||||
* @param isDisabled if true, sets VSCodeVim to Disabled mode; else sets to enabled mode
|
||||
*/
|
||||
async function toggleExtension(isDisabled: boolean, compositionState: CompositionState) {
|
||||
await VSCodeContext.set('vim.active', !isDisabled);
|
||||
const mh = await getAndUpdateModeHandler();
|
||||
if (mh) {
|
||||
if (isDisabled) {
|
||||
await mh.handleKeyEvent(SpecialKeys.ExtensionDisable);
|
||||
compositionState.reset();
|
||||
ModeHandlerMap.clear();
|
||||
} else {
|
||||
await mh.handleKeyEvent(SpecialKeys.ExtensionEnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function overrideCommand(
|
||||
context: vscode.ExtensionContext,
|
||||
command: string,
|
||||
callback: (...args: any[]) => any,
|
||||
) {
|
||||
const disposable = vscode.commands.registerCommand(command, async (args) => {
|
||||
if (configuration.disableExtension) {
|
||||
return vscode.commands.executeCommand('default:' + command, args);
|
||||
}
|
||||
|
||||
if (!vscode.window.activeTextEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
vscode.window.activeTextEditor.document &&
|
||||
vscode.window.activeTextEditor.document.uri.toString() === 'debug:input'
|
||||
) {
|
||||
return vscode.commands.executeCommand('default:' + command, args);
|
||||
}
|
||||
|
||||
return callback(args) as vscode.Disposable;
|
||||
});
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
export function registerCommand(
|
||||
context: vscode.ExtensionContext,
|
||||
command: string,
|
||||
callback: (...args: any[]) => any,
|
||||
requiresActiveEditor: boolean = true,
|
||||
) {
|
||||
const disposable = vscode.commands.registerCommand(command, async (args) => {
|
||||
if (requiresActiveEditor && !vscode.window.activeTextEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(args);
|
||||
});
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
export function registerEventListener<T>(
|
||||
context: vscode.ExtensionContext,
|
||||
event: vscode.Event<T>,
|
||||
listener: (e: T) => void,
|
||||
exitOnExtensionDisable = true,
|
||||
exitOnTests = false,
|
||||
) {
|
||||
const disposable = event(async (e) => {
|
||||
if (exitOnExtensionDisable && configuration.disableExtension) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (exitOnTests && Globals.isTesting) {
|
||||
return;
|
||||
}
|
||||
|
||||
listener(e);
|
||||
});
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if there was a remap being executed to stop
|
||||
*/
|
||||
async function forceStopRecursiveRemap(mh: ModeHandler): Promise<boolean> {
|
||||
if (mh.remapState.isCurrentlyPerformingRecursiveRemapping) {
|
||||
mh.remapState.forceStopRecursiveRemapping = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
22
extensionWeb.ts
Normal file
22
extensionWeb.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Extension.ts is a lightweight wrapper around ModeHandler. It converts key
|
||||
* events to their string names and passes them on to ModeHandler via
|
||||
* handleKeyEvent().
|
||||
*/
|
||||
import './src/actions/include-main';
|
||||
|
||||
/**
|
||||
* Load configuration validator
|
||||
*/
|
||||
|
||||
import './src/configuration/validators/inputMethodSwitcherValidator';
|
||||
import './src/configuration/validators/remappingValidator';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { activate as activateFunc } from './extensionBase';
|
||||
|
||||
require('setimmediate');
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
void activateFunc(context, false);
|
||||
}
|
316
package-lock.json
generated
316
package-lock.json
generated
@ -1,13 +1,24 @@
|
||||
{
|
||||
"name": "vscode-helix-emulation",
|
||||
"version": "0.5.4",
|
||||
"version": "0.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vscode-helix-emulation",
|
||||
"version": "0.5.4",
|
||||
"version": "0.6.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"os-browserify": "0.3.0",
|
||||
"parsimmon": "^1.18.1",
|
||||
"path-browserify": "1.0.1",
|
||||
"process": "0.11.10",
|
||||
"queue": "^7.0.0",
|
||||
"untildify": "4.0.0",
|
||||
"util": "0.12.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/http-errors": "^1.8.0",
|
||||
"@types/node": "^20.8.9",
|
||||
@ -974,6 +985,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
|
||||
"dependencies": {
|
||||
"possible-typed-array-names": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/azure-devops-node-api": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz",
|
||||
@ -1133,13 +1158,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@ -1328,6 +1358,22 @@
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/define-properties": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
|
||||
@ -1353,6 +1399,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/diff-match-patch": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
|
||||
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@ -1491,6 +1542,25 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-to-primitive": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
|
||||
@ -2309,6 +2379,14 @@
|
||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
"integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
|
||||
"dependencies": {
|
||||
"is-callable": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
@ -2336,10 +2414,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/function.prototype.name": {
|
||||
"version": "1.1.5",
|
||||
@ -2369,14 +2449,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
|
||||
"integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
|
||||
"dev": true,
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.3"
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@ -2480,6 +2564,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/grapheme-splitter": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
|
||||
@ -2523,12 +2618,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/has-property-descriptors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
|
||||
"integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
|
||||
"dev": true,
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.1"
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-proto": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
||||
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@ -2538,7 +2643,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@ -2547,12 +2651,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
|
||||
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
|
||||
"dev": true,
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.2"
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -2561,6 +2664,17 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hosted-git-info": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
|
||||
@ -2659,8 +2773,7 @@
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
@ -2682,6 +2795,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arguments": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
||||
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-tostringtag": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-bigint": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
|
||||
@ -2714,7 +2842,6 @@
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@ -2746,6 +2873,20 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-generator-function": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
|
||||
"integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
|
||||
"dependencies": {
|
||||
"has-tostringtag": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@ -2861,6 +3002,20 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-typed-array": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
|
||||
"integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
|
||||
"dependencies": {
|
||||
"which-typed-array": "^1.1.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-weakref": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
|
||||
@ -2963,6 +3118,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@ -3222,6 +3382,11 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/os-browserify": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
|
||||
"integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@ -3298,6 +3463,16 @@
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parsimmon": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz",
|
||||
"integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@ -3352,6 +3527,14 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
|
||||
"integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
|
||||
@ -3402,6 +3585,14 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
@ -3458,6 +3649,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/queue": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/queue/-/queue-7.0.0.tgz",
|
||||
"integrity": "sha512-sphwS7HdfQnvrJAXUNAUgpf9H/546IE3p/5Lf2jr71O4udEYlqAhkevykumas2FYuMkX/29JMOgrRdRoYZ/X9w=="
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@ -3654,6 +3850,22 @@
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"gopd": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@ -4064,6 +4276,14 @@
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/untildify": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
||||
"integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@ -4079,6 +4299,18 @@
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"is-arguments": "^1.0.4",
|
||||
"is-generator-function": "^1.0.7",
|
||||
"is-typed-array": "^1.1.3",
|
||||
"which-typed-array": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@ -4159,6 +4391,24 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/which-typed-array": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
|
||||
"integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
|
||||
"dependencies": {
|
||||
"available-typed-arrays": "^1.0.7",
|
||||
"call-bind": "^1.0.7",
|
||||
"for-each": "^0.3.3",
|
||||
"gopd": "^1.0.1",
|
||||
"has-tostringtag": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
|
16
package.json
16
package.json
@ -244,11 +244,6 @@
|
||||
"command": "deleteWordRight",
|
||||
"when": "editorTextFocus && extension.helixKeymap.insertMode"
|
||||
},
|
||||
{
|
||||
"key": "alt+d",
|
||||
"command": "deleteWordRight",
|
||||
"when": "editorTextFocus && extension.helixKeymap.insertMode"
|
||||
},
|
||||
{
|
||||
"key": "ctrl+u",
|
||||
"command": "deleteAllLeft",
|
||||
@ -314,6 +309,17 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"release": "bumpp && npm run publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"parsimmon": "^1.18.1",
|
||||
"path-browserify": "1.0.1",
|
||||
"os-browserify": "0.3.0",
|
||||
"untildify": "4.0.0",
|
||||
"process": "0.11.10",
|
||||
"util": "0.12.5",
|
||||
"lodash": "^4.17.21",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"queue": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/http-errors": "^1.8.0",
|
||||
"@types/node": "^20.8.9",
|
||||
|
@ -1,138 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class SymbolProvider {
|
||||
/** The array of symbols in the current document */
|
||||
tree: vscode.DocumentSymbol[] = [];
|
||||
/** Current index in tree */
|
||||
symbolIndex = 0;
|
||||
/** Flag for if the tree is dirty or not */
|
||||
dirtyTree = false;
|
||||
|
||||
static checkSymbolKindPermitted(symbolKind: vscode.SymbolKind): boolean {
|
||||
// https://code.visualstudio.com/api/references/vscode-api#SymbolKind
|
||||
return (
|
||||
symbolKind === vscode.SymbolKind.Constructor ||
|
||||
symbolKind === vscode.SymbolKind.Enum ||
|
||||
symbolKind === vscode.SymbolKind.EnumMember ||
|
||||
symbolKind === vscode.SymbolKind.Event ||
|
||||
symbolKind === vscode.SymbolKind.Function ||
|
||||
symbolKind === vscode.SymbolKind.Interface ||
|
||||
symbolKind === vscode.SymbolKind.Method
|
||||
);
|
||||
}
|
||||
|
||||
async refreshTree(uri: vscode.Uri) {
|
||||
const results = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
|
||||
'vscode.executeDocumentSymbolProvider',
|
||||
uri,
|
||||
);
|
||||
if (!results) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const flattenedSymbols: vscode.DocumentSymbol[] = [];
|
||||
const addSymbols = (flattenedSymbols: vscode.DocumentSymbol[], results: vscode.DocumentSymbol[]) => {
|
||||
results.forEach((symbol: vscode.DocumentSymbol) => {
|
||||
if (SymbolProvider.checkSymbolKindPermitted(symbol.kind)) {
|
||||
flattenedSymbols.push(symbol);
|
||||
}
|
||||
if (symbol.children && symbol.children.length > 0) {
|
||||
addSymbols(flattenedSymbols, symbol.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
addSymbols(flattenedSymbols, results);
|
||||
|
||||
this.tree = flattenedSymbols.sort((x: vscode.DocumentSymbol, y: vscode.DocumentSymbol) => {
|
||||
const lineDiff = x.selectionRange.start.line - y.selectionRange.start.line;
|
||||
if (lineDiff === 0) {
|
||||
return x.selectionRange.start.character - y.selectionRange.start.character;
|
||||
}
|
||||
return lineDiff;
|
||||
});
|
||||
|
||||
this.dirtyTree = false;
|
||||
}
|
||||
|
||||
getContainingSymbolIndex(position: vscode.Position): number | undefined {
|
||||
if (this.tree.length === 0 || this.dirtyTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
const symbolIndex = this.tree.findIndex((symbol: vscode.DocumentSymbol) => {
|
||||
return symbol.range.contains(position);
|
||||
});
|
||||
|
||||
return symbolIndex;
|
||||
}
|
||||
|
||||
getContainingSymbolRange(position: vscode.Position): vscode.Range | undefined {
|
||||
if (this.tree.length === 0 || this.dirtyTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
const symbolIndex = this.getContainingSymbolIndex(position);
|
||||
if (symbolIndex === -1 || symbolIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const symbol = this.tree[symbolIndex];
|
||||
|
||||
if (symbol) {
|
||||
return symbol.range;
|
||||
}
|
||||
}
|
||||
|
||||
getNextFunctionRange(editor: vscode.TextEditor): vscode.Range | undefined {
|
||||
if (this.tree.length === 0 || this.dirtyTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeCursor = editor.selection.active;
|
||||
const currentSymbolIndex = this.getContainingSymbolIndex(activeCursor);
|
||||
if (currentSymbolIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Iterate forward until we find the next function on the same level
|
||||
for (let i = currentSymbolIndex + 1; i < this.tree.length; i++) {
|
||||
if (this.tree[i].kind === vscode.SymbolKind.Function) {
|
||||
this.symbolIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const symbol = this.tree[this.symbolIndex];
|
||||
if (symbol) {
|
||||
return symbol.range;
|
||||
}
|
||||
}
|
||||
|
||||
getPreviousFunctionRange(editor: vscode.TextEditor): vscode.Range | undefined {
|
||||
if (this.tree.length === 0 || this.dirtyTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeCursor = editor.selection.active;
|
||||
const currentSymbolIndex = this.getContainingSymbolIndex(activeCursor);
|
||||
if (currentSymbolIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Iterate backwards until we find the previouis function on the same level
|
||||
for (let i = currentSymbolIndex - 1; i > 0; i--) {
|
||||
if (this.tree[i].kind === vscode.SymbolKind.Function) {
|
||||
this.symbolIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const symbol = this.tree[this.symbolIndex];
|
||||
if (symbol) {
|
||||
return symbol.range;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const symbolProvider = new SymbolProvider();
|
@ -1,6 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { HelixState } from './helix_state_types';
|
||||
import { ParseKeysStatus } from './parse_keys_types';
|
||||
|
||||
export type Action = (vimState: HelixState, keys: string[], editor: vscode.TextEditor) => ParseKeysStatus;
|
@ -1,498 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Action } from '../action_types';
|
||||
import { HelixState } from '../helix_state_types';
|
||||
import {
|
||||
enterInsertMode,
|
||||
enterNormalMode,
|
||||
enterSearchMode,
|
||||
enterSelectMode,
|
||||
enterVisualLineMode,
|
||||
enterVisualMode,
|
||||
setModeCursorStyle,
|
||||
} from '../modes';
|
||||
import { Mode } from '../modes_types';
|
||||
import { parseKeysExact, parseKeysRegex } from '../parse_keys';
|
||||
import * as positionUtils from '../position_utils';
|
||||
import { putAfter } from '../put_utils/put_after';
|
||||
import { putBefore } from '../put_utils/put_before';
|
||||
import { removeTypeSubscription } from '../type_subscription';
|
||||
import { flashYankHighlight } from '../yank_highlight';
|
||||
import { gotoActions } from './gotoMode';
|
||||
import KeyMap from './keymaps';
|
||||
import { matchActions } from './matchMode';
|
||||
import { isSingleLineRange, yank } from './operators';
|
||||
import { spaceActions } from './spaceMode';
|
||||
import { unimparedActions } from './unimpared';
|
||||
import { viewActions } from './viewMode';
|
||||
import { windowActions } from './windowMode';
|
||||
|
||||
enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
export const actions: Action[] = [
|
||||
parseKeysExact(['p'], [Mode.Occurrence], () => {
|
||||
vscode.commands.executeCommand('editor.action.addSelectionToPreviousFindMatch');
|
||||
}),
|
||||
|
||||
parseKeysExact(['a'], [Mode.Occurrence], () => {
|
||||
vscode.commands.executeCommand('editor.action.selectHighlights');
|
||||
}),
|
||||
|
||||
parseKeysExact(['n'], [Mode.Normal], (helixState) => {
|
||||
if (helixState.searchState.selectModeActive) {
|
||||
vscode.commands.executeCommand('actions.findWithSelection');
|
||||
helixState.searchState.selectModeActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
vscode.commands.executeCommand('editor.action.nextMatchFindAction');
|
||||
}),
|
||||
|
||||
parseKeysExact(['N'], [Mode.Normal], (helixState) => {
|
||||
if (helixState.searchState.selectModeActive) {
|
||||
vscode.commands.executeCommand('actions.findWithSelection');
|
||||
helixState.searchState.selectModeActive = false;
|
||||
return;
|
||||
}
|
||||
vscode.commands.executeCommand('editor.action.previousMatchFindAction');
|
||||
}),
|
||||
|
||||
parseKeysExact(['?'], [Mode.Normal], (helixState) => {
|
||||
enterSearchMode(helixState);
|
||||
helixState.searchState.previousSearchResult(helixState);
|
||||
}),
|
||||
|
||||
// Selection Stuff
|
||||
parseKeysExact(['s'], [Mode.Normal, Mode.Visual], (helixState, editor) => {
|
||||
enterSelectMode(helixState);
|
||||
// if we enter select mode we should save the current selection
|
||||
helixState.currentSelection = editor.selection;
|
||||
}),
|
||||
|
||||
parseKeysExact([','], [Mode.Normal, Mode.Visual], (_, editor) => {
|
||||
// Keep primary selection only
|
||||
editor.selections = editor.selections.slice(0, 1);
|
||||
}),
|
||||
|
||||
parseKeysExact(['/'], [Mode.Normal], (helixState) => {
|
||||
enterSearchMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['*'], [Mode.Normal], (_) => {
|
||||
vscode.commands.executeCommand('actions.findWithSelection');
|
||||
}),
|
||||
|
||||
parseKeysExact(['>'], [Mode.Normal, Mode.Visual], (_) => {
|
||||
vscode.commands.executeCommand('editor.action.indentLines');
|
||||
}),
|
||||
|
||||
parseKeysExact(['<'], [Mode.Normal, Mode.Visual], (_) => {
|
||||
vscode.commands.executeCommand('editor.action.outdentLines');
|
||||
}),
|
||||
|
||||
parseKeysExact(['='], [Mode.Normal, Mode.Visual], (_) => {
|
||||
vscode.commands.executeCommand('editor.action.formatSelection');
|
||||
}),
|
||||
|
||||
parseKeysExact(['`'], [Mode.Normal], (vimState, editor) => {
|
||||
// Take the selection and make it all lowercase
|
||||
editor.edit((editBuilder) => {
|
||||
editor.selections.forEach((selection) => {
|
||||
const text = editor.document.getText(selection);
|
||||
editBuilder.replace(selection, text.toLowerCase());
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['~'], [Mode.Normal], (vimState, editor) => {
|
||||
// Switch the case of the selection (so if upper case make lower case and vice versa)
|
||||
editor.edit((editBuilder) => {
|
||||
editor.selections.forEach((selection) => {
|
||||
const text = editor.document.getText(selection);
|
||||
editBuilder.replace(
|
||||
selection,
|
||||
text.replace(/./g, (c) => (c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase())),
|
||||
);
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
// replace
|
||||
parseKeysRegex(/^r(.)/, /^r/, [Mode.Normal], (helixState, editor, match) => {
|
||||
const position = editor.selection.active;
|
||||
editor.edit((builder) => {
|
||||
builder.replace(new vscode.Range(position, position.with({ character: position.character + 1 })), match[1]);
|
||||
});
|
||||
}),
|
||||
|
||||
// existing
|
||||
parseKeysExact(
|
||||
[KeyMap.Actions.InsertMode],
|
||||
[Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.Occurrence],
|
||||
(vimState, editor) => {
|
||||
enterInsertMode(vimState);
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
removeTypeSubscription(vimState);
|
||||
},
|
||||
),
|
||||
|
||||
parseKeysExact([KeyMap.Actions.InsertAtLineStart], [Mode.Normal], (vimState, editor) => {
|
||||
editor.selections = editor.selections.map((selection) => {
|
||||
const character = editor.document.lineAt(selection.active.line).firstNonWhitespaceCharacterIndex;
|
||||
const newPosition = selection.active.with({ character: character });
|
||||
return new vscode.Selection(newPosition, newPosition);
|
||||
});
|
||||
|
||||
enterInsertMode(vimState);
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
removeTypeSubscription(vimState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['a'], [Mode.Normal], (vimState, editor) => {
|
||||
enterInsertMode(vimState, false);
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
removeTypeSubscription(vimState);
|
||||
}),
|
||||
|
||||
parseKeysExact([KeyMap.Actions.InsertAtLineEnd], [Mode.Normal], (vimState, editor) => {
|
||||
editor.selections = editor.selections.map((selection) => {
|
||||
const lineLength = editor.document.lineAt(selection.active.line).text.length;
|
||||
const newPosition = selection.active.with({ character: lineLength });
|
||||
return new vscode.Selection(newPosition, newPosition);
|
||||
});
|
||||
|
||||
enterInsertMode(vimState);
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
removeTypeSubscription(vimState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['v'], [Mode.Normal, Mode.VisualLine], (vimState, editor) => {
|
||||
enterVisualMode(vimState);
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
}),
|
||||
|
||||
parseKeysExact(['x'], [Mode.Normal, Mode.Visual], () => {
|
||||
vscode.commands.executeCommand('expandLineSelection');
|
||||
}),
|
||||
|
||||
parseKeysExact([KeyMap.Actions.NewLineBelow], [Mode.Normal], (vimState, editor) => {
|
||||
enterInsertMode(vimState);
|
||||
vscode.commands.executeCommand('editor.action.insertLineAfter');
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
removeTypeSubscription(vimState);
|
||||
}),
|
||||
|
||||
parseKeysExact([KeyMap.Actions.NewLineAbove], [Mode.Normal], (vimState, editor) => {
|
||||
enterInsertMode(vimState);
|
||||
vscode.commands.executeCommand('editor.action.insertLineBefore');
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
removeTypeSubscription(vimState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['p'], [Mode.Normal, Mode.Visual, Mode.VisualLine], putAfter),
|
||||
parseKeysExact(['P'], [Mode.Normal], putBefore),
|
||||
|
||||
parseKeysExact(['u'], [Mode.Normal, Mode.Visual, Mode.VisualLine], () => {
|
||||
vscode.commands.executeCommand('undo');
|
||||
}),
|
||||
|
||||
parseKeysExact(['U'], [Mode.Normal, Mode.Visual, Mode.VisualLine], () => {
|
||||
vscode.commands.executeCommand('redo');
|
||||
}),
|
||||
|
||||
parseKeysExact(['d', 'd'], [Mode.Normal], (vimState, editor) => {
|
||||
deleteLine(vimState, editor);
|
||||
}),
|
||||
|
||||
parseKeysExact(['D'], [Mode.Normal], () => {
|
||||
vscode.commands.executeCommand('deleteAllRight');
|
||||
}),
|
||||
|
||||
parseKeysRegex(/(\\d+)g/, /^g$/, [Mode.Normal, Mode.Visual], (helixState, editor, match) => {
|
||||
new vscode.Position(parseInt(match[1]), 0);
|
||||
}),
|
||||
|
||||
// add 1 character swap
|
||||
parseKeysRegex(/^x(.)$/, /^x$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => {
|
||||
editor.edit((builder) => {
|
||||
editor.selections.forEach((s) => {
|
||||
const oneChar = s.with({
|
||||
end: s.active.with({
|
||||
character: s.active.character + 1,
|
||||
}),
|
||||
});
|
||||
builder.replace(oneChar, match[1]);
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
// same for rip command
|
||||
parseKeysRegex(
|
||||
RegExp(`^r(\\d+)(${KeyMap.Motions.MoveUp}|${KeyMap.Motions.MoveDown})$`),
|
||||
/^(r|r\d+)$/,
|
||||
[Mode.Normal, Mode.Visual],
|
||||
(vimState, editor, match) => {
|
||||
const lineCount = parseInt(match[1]);
|
||||
const direction = match[2] == KeyMap.Motions.MoveUp ? Direction.Up : Direction.Down;
|
||||
// console.log(`delete ${lineCount} lines up`);
|
||||
const selections = makeMultiLineSelection(vimState, editor, lineCount, direction);
|
||||
|
||||
yank(vimState, editor, selections, true);
|
||||
|
||||
deleteLines(vimState, editor, lineCount, direction);
|
||||
},
|
||||
),
|
||||
|
||||
// same for duplicate command
|
||||
parseKeysRegex(
|
||||
RegExp(`^q(\\d+)(${KeyMap.Motions.MoveUp}|${KeyMap.Motions.MoveDown})$`),
|
||||
/^(q|q\d+)$/,
|
||||
[Mode.Normal, Mode.Visual],
|
||||
(vimState, editor, match) => {
|
||||
const lineCount = parseInt(match[1]);
|
||||
const direction = match[2] == KeyMap.Motions.MoveUp ? Direction.Up : Direction.Down;
|
||||
// console.log(`delete ${lineCount} lines up`);
|
||||
editor.selections = makeMultiLineSelection(vimState, editor, lineCount, direction);
|
||||
vscode.commands.executeCommand('editor.action.copyLinesDownAction');
|
||||
},
|
||||
),
|
||||
|
||||
parseKeysExact(['c', 'c'], [Mode.Normal], (vimState, editor) => {
|
||||
editor.edit((editBuilder) => {
|
||||
editor.selections.forEach((selection) => {
|
||||
const line = editor.document.lineAt(selection.active.line);
|
||||
editBuilder.delete(
|
||||
new vscode.Range(
|
||||
selection.active.with({
|
||||
character: line.firstNonWhitespaceCharacterIndex,
|
||||
}),
|
||||
selection.active.with({ character: line.text.length }),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
enterInsertMode(vimState);
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
removeTypeSubscription(vimState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['C'], [Mode.Normal], (vimState, editor) => {
|
||||
vscode.commands.executeCommand('deleteAllRight');
|
||||
enterInsertMode(vimState);
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
removeTypeSubscription(vimState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['y', 'y'], [Mode.Normal], (vimState, editor) => {
|
||||
yankLine(vimState, editor);
|
||||
|
||||
// Yank highlight
|
||||
const highlightRanges = editor.selections.map((selection) => {
|
||||
const lineLength = editor.document.lineAt(selection.active.line).text.length;
|
||||
return new vscode.Range(
|
||||
selection.active.with({ character: 0 }),
|
||||
selection.active.with({ character: lineLength }),
|
||||
);
|
||||
});
|
||||
flashYankHighlight(editor, highlightRanges);
|
||||
}),
|
||||
|
||||
parseKeysExact(['y'], [Mode.Normal, Mode.Visual], (vimState, editor) => {
|
||||
// Yank highlight
|
||||
const highlightRanges = editor.selections.map((selection) => selection.with());
|
||||
|
||||
// We need to detect if the ranges are lines because we need to handle them differently
|
||||
highlightRanges.every((range) => isSingleLineRange(range));
|
||||
yank(vimState, editor, highlightRanges, false);
|
||||
flashYankHighlight(editor, highlightRanges);
|
||||
if (vimState.mode === Mode.Visual) {
|
||||
enterNormalMode(vimState);
|
||||
}
|
||||
}),
|
||||
|
||||
parseKeysExact(['q', 'q'], [Mode.Normal, Mode.Visual], () => {
|
||||
vscode.commands.executeCommand('editor.action.copyLinesDownAction');
|
||||
}),
|
||||
|
||||
parseKeysExact(['Q', 'Q'], [Mode.Normal, Mode.Visual], () => {
|
||||
vscode.commands.executeCommand('editor.action.copyLinesUpAction');
|
||||
}),
|
||||
|
||||
parseKeysExact(['r', 'r'], [Mode.Normal], (vimState, editor) => {
|
||||
yankLine(vimState, editor);
|
||||
deleteLine(vimState, editor);
|
||||
}),
|
||||
|
||||
parseKeysExact(['s', 's'], [Mode.Normal], (vimState, editor) => {
|
||||
editor.selections = editor.selections.map((selection) => {
|
||||
return new vscode.Selection(
|
||||
selection.active.with({ character: 0 }),
|
||||
positionUtils.lineEnd(editor.document, selection.active),
|
||||
);
|
||||
});
|
||||
|
||||
enterVisualLineMode(vimState);
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
}),
|
||||
|
||||
parseKeysExact(['S'], [Mode.Normal], (vimState, editor) => {
|
||||
editor.selections = editor.selections.map((selection) => {
|
||||
return new vscode.Selection(selection.active, positionUtils.lineEnd(editor.document, selection.active));
|
||||
});
|
||||
|
||||
enterVisualMode(vimState);
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
}),
|
||||
|
||||
// parseKeysExact(['h'], [Mode.Normal], (vimState, editor) => {
|
||||
// vscode.commands.executeCommand('deleteLeft')
|
||||
// }),
|
||||
|
||||
parseKeysExact([';'], [Mode.Normal], (vimState, editor) => {
|
||||
const active = editor.selection.active;
|
||||
editor.selection = new vscode.Selection(active, active);
|
||||
}),
|
||||
|
||||
...gotoActions,
|
||||
...windowActions,
|
||||
...viewActions,
|
||||
...spaceActions,
|
||||
...matchActions,
|
||||
...unimparedActions,
|
||||
];
|
||||
|
||||
function makeMultiLineSelection(
|
||||
vimState: HelixState,
|
||||
editor: vscode.TextEditor,
|
||||
lineCount: number,
|
||||
direction: Direction,
|
||||
): vscode.Selection[] {
|
||||
return editor.selections.map((selection) => {
|
||||
if (direction == Direction.Up) {
|
||||
const endLine = selection.active.line - lineCount + 1;
|
||||
const startPos = positionUtils.lineEnd(editor.document, selection.active);
|
||||
const endPos = endLine >= 0 ? new vscode.Position(endLine, 0) : new vscode.Position(0, 0);
|
||||
return new vscode.Selection(startPos, endPos);
|
||||
} else {
|
||||
const endLine = selection.active.line + lineCount - 1;
|
||||
const startPos = new vscode.Position(selection.active.line, 0);
|
||||
const endPos =
|
||||
endLine < editor.document.lineCount
|
||||
? new vscode.Position(endLine, editor.document.lineAt(endLine).text.length)
|
||||
: positionUtils.lastChar(editor.document);
|
||||
|
||||
return new vscode.Selection(startPos, endPos);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteLines(
|
||||
vimState: HelixState,
|
||||
editor: vscode.TextEditor,
|
||||
lineCount: number,
|
||||
direction: Direction = Direction.Down,
|
||||
): void {
|
||||
const selections = editor.selections.map((selection) => {
|
||||
if (direction == Direction.Up) {
|
||||
const endLine = selection.active.line - lineCount;
|
||||
if (endLine >= 0) {
|
||||
const startPos = positionUtils.lineEnd(editor.document, selection.active);
|
||||
const endPos = new vscode.Position(endLine, editor.document.lineAt(endLine).text.length);
|
||||
return new vscode.Selection(startPos, endPos);
|
||||
} else {
|
||||
const startPos =
|
||||
selection.active.line + 1 <= editor.document.lineCount
|
||||
? new vscode.Position(selection.active.line + 1, 0)
|
||||
: positionUtils.lineEnd(editor.document, selection.active);
|
||||
|
||||
const endPos = new vscode.Position(0, 0);
|
||||
return new vscode.Selection(startPos, endPos);
|
||||
}
|
||||
} else {
|
||||
const endLine = selection.active.line + lineCount;
|
||||
if (endLine <= editor.document.lineCount - 1) {
|
||||
const startPos = new vscode.Position(selection.active.line, 0);
|
||||
const endPos = new vscode.Position(endLine, 0);
|
||||
return new vscode.Selection(startPos, endPos);
|
||||
} else {
|
||||
const startPos =
|
||||
selection.active.line - 1 >= 0
|
||||
? new vscode.Position(
|
||||
selection.active.line - 1,
|
||||
editor.document.lineAt(selection.active.line - 1).text.length,
|
||||
)
|
||||
: new vscode.Position(selection.active.line, 0);
|
||||
|
||||
const endPos = positionUtils.lastChar(editor.document);
|
||||
return new vscode.Selection(startPos, endPos);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
editor
|
||||
.edit((builder) => {
|
||||
selections.forEach((sel) => builder.replace(sel, ''));
|
||||
})
|
||||
.then(() => {
|
||||
editor.selections = editor.selections.map((selection) => {
|
||||
const character = editor.document.lineAt(selection.active.line).firstNonWhitespaceCharacterIndex;
|
||||
const newPosition = selection.active.with({ character: character });
|
||||
return new vscode.Selection(newPosition, newPosition);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteLine(vimState: HelixState, editor: vscode.TextEditor, direction: Direction = Direction.Down): void {
|
||||
deleteLines(vimState, editor, 1, direction);
|
||||
}
|
||||
|
||||
function yankLine(vimState: HelixState, editor: vscode.TextEditor): void {
|
||||
vimState.registers = {
|
||||
contentsList: editor.selections.map((selection) => {
|
||||
return editor.document.lineAt(selection.active.line).text;
|
||||
}),
|
||||
linewise: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function switchToUppercase(editor: vscode.TextEditor): void {
|
||||
editor.edit((editBuilder) => {
|
||||
editor.selections.forEach((selection) => {
|
||||
const text = editor.document.getText(selection);
|
||||
editBuilder.replace(selection, text.toUpperCase());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function incremenet(editor: vscode.TextEditor): void {
|
||||
// Move the cursor to the first number and incremene the number
|
||||
// If the cursor is not on a number, then do nothing
|
||||
editor.edit((editBuilder) => {
|
||||
editor.selections.forEach((selection) => {
|
||||
const translatedSelection = selection.with(selection.start, selection.start.translate(0, 1));
|
||||
const text = editor.document.getText(translatedSelection);
|
||||
const number = parseInt(text, 10);
|
||||
if (!isNaN(number)) {
|
||||
editBuilder.replace(translatedSelection, (number + 1).toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function decrement(editor: vscode.TextEditor): void {
|
||||
// Move the cursor to the first number and incremene the number
|
||||
// If the cursor is not on a number, then do nothing
|
||||
editor.edit((editBuilder) => {
|
||||
editor.selections.forEach((selection) => {
|
||||
const translatedSelection = selection.with(selection.start, selection.start.translate(0, 1));
|
||||
const text = editor.document.getText(translatedSelection);
|
||||
const number = parseInt(text, 10);
|
||||
if (!isNaN(number)) {
|
||||
editBuilder.replace(translatedSelection, (number - 1).toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
294
src/actions/base.ts
Normal file
294
src/actions/base.ts
Normal file
@ -0,0 +1,294 @@
|
||||
import { Position } from 'vscode';
|
||||
import { Cursor } from '../common/motion/cursor';
|
||||
import { Notation } from '../configuration/notation';
|
||||
import { ActionType, IBaseAction } from './types';
|
||||
import { isTextTransformation } from '../transformations/transformations';
|
||||
import { configuration } from './../configuration/configuration';
|
||||
import { Mode } from './../mode/mode';
|
||||
import { VimState } from './../state/vimState';
|
||||
|
||||
export abstract class BaseAction implements IBaseAction {
|
||||
abstract readonly actionType: ActionType;
|
||||
|
||||
public name = '';
|
||||
|
||||
/**
|
||||
* If true, the cursor position will be added to the jump list on completion.
|
||||
*/
|
||||
public readonly isJump: boolean = false;
|
||||
|
||||
/**
|
||||
* If true, the action will create an undo point.
|
||||
*/
|
||||
public readonly createsUndoPoint: boolean = false;
|
||||
|
||||
/**
|
||||
* If this is being run in multi cursor mode, the index of the cursor
|
||||
* this action is being applied to.
|
||||
*/
|
||||
public multicursorIndex: number | undefined;
|
||||
|
||||
/**
|
||||
* Whether we should change `vimState.desiredColumn`
|
||||
*/
|
||||
public readonly preservesDesiredColumn: boolean = false;
|
||||
|
||||
/**
|
||||
* Modes that this action can be run in.
|
||||
*/
|
||||
public abstract readonly modes: readonly Mode[];
|
||||
|
||||
/**
|
||||
* The sequence of keys you use to trigger the action, or a list of such sequences.
|
||||
*/
|
||||
public abstract readonly keys: readonly string[] | readonly string[][];
|
||||
|
||||
/**
|
||||
* The keys pressed at the time that this action was triggered.
|
||||
*/
|
||||
// TODO: make readonly
|
||||
public keysPressed: string[] = [];
|
||||
|
||||
private static readonly isSingleNumber: RegExp = /^[0-9]$/;
|
||||
private static readonly isSingleAlpha: RegExp = /^[a-zA-Z]$/;
|
||||
|
||||
/**
|
||||
* Is this action valid in the current Vim state?
|
||||
*/
|
||||
public doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
if (
|
||||
vimState.currentModeIncludingPseudoModes === Mode.OperatorPendingMode &&
|
||||
this.actionType === 'command'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.modes.includes(vimState.currentMode) &&
|
||||
BaseAction.CompareKeypressSequence(this.keys, keysPressed)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Could the user be in the process of doing this action.
|
||||
*/
|
||||
public couldActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
if (
|
||||
vimState.currentModeIncludingPseudoModes === Mode.OperatorPendingMode &&
|
||||
this.actionType === 'command'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.modes.includes(vimState.currentMode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys2D = BaseAction.is2DArray(this.keys) ? this.keys : [this.keys];
|
||||
const keysSlice = keys2D.map((x) => x.slice(0, keysPressed.length));
|
||||
if (!BaseAction.CompareKeypressSequence(keysSlice, keysPressed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static CompareKeypressSequence(
|
||||
one: readonly string[] | readonly string[][],
|
||||
two: readonly string[],
|
||||
): boolean {
|
||||
if (BaseAction.is2DArray(one)) {
|
||||
for (const sequence of one) {
|
||||
if (BaseAction.CompareKeypressSequence(sequence, two)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (one.length !== two.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0, j = 0; i < one.length; i++, j++) {
|
||||
const left = one[i];
|
||||
const right = two[j];
|
||||
|
||||
if (left === right && right !== configuration.leader) {
|
||||
continue;
|
||||
} else if (left === '<any>') {
|
||||
continue;
|
||||
} else if (left === '<leader>' && right === configuration.leader) {
|
||||
continue;
|
||||
} else if (left === '<number>' && this.isSingleNumber.test(right)) {
|
||||
continue;
|
||||
} else if (left === '<alpha>' && this.isSingleAlpha.test(right)) {
|
||||
continue;
|
||||
} else if (left === '<character>' && !Notation.IsControlKey(right)) {
|
||||
continue;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.keys.join('');
|
||||
}
|
||||
|
||||
private static is2DArray<T>(x: readonly T[] | readonly T[][]): x is readonly T[][] {
|
||||
return Array.isArray(x[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A command is something like <Esc>, :, v, i, etc.
|
||||
*/
|
||||
export abstract class BaseCommand extends BaseAction {
|
||||
override actionType: ActionType = 'command' as const;
|
||||
|
||||
/**
|
||||
* If isCompleteAction is true, then triggering this command is a complete action -
|
||||
* that means that we'll go and try to run it.
|
||||
*/
|
||||
public isCompleteAction = true;
|
||||
|
||||
/**
|
||||
* In multi-cursor mode, do we run this command for every cursor, or just once?
|
||||
*/
|
||||
public runsOnceForEveryCursor(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, exec() will get called N times where N is the count.
|
||||
*
|
||||
* If false, exec() will only be called once, and you are expected to
|
||||
* handle count prefixes (e.g. the 3 in 3w) yourself.
|
||||
*/
|
||||
public readonly runsOnceForEachCountPrefix: boolean = false;
|
||||
|
||||
/**
|
||||
* Run the command a single time.
|
||||
*/
|
||||
public async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
throw new Error('Not implemented!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the command the number of times VimState wants us to.
|
||||
*/
|
||||
public async execCount(position: Position, vimState: VimState): Promise<void> {
|
||||
const timesToRepeat = this.runsOnceForEachCountPrefix ? vimState.recordedState.count || 1 : 1;
|
||||
|
||||
if (!this.runsOnceForEveryCursor()) {
|
||||
for (let i = 0; i < timesToRepeat; i++) {
|
||||
await this.exec(position, vimState);
|
||||
}
|
||||
|
||||
for (const transformation of vimState.recordedState.transformer.transformations) {
|
||||
if (isTextTransformation(transformation) && transformation.cursorIndex === undefined) {
|
||||
transformation.cursorIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const resultingCursors: Cursor[] = [];
|
||||
|
||||
const cursorsToIterateOver = vimState.cursors
|
||||
.map((x) => new Cursor(x.start, x.stop))
|
||||
.sort((a, b) =>
|
||||
a.start.line > b.start.line ||
|
||||
(a.start.line === b.start.line && a.start.character > b.start.character)
|
||||
? 1
|
||||
: -1,
|
||||
);
|
||||
|
||||
let cursorIndex = 0;
|
||||
for (const { start, stop } of cursorsToIterateOver) {
|
||||
this.multicursorIndex = cursorIndex++;
|
||||
|
||||
vimState.cursorStopPosition = stop;
|
||||
vimState.cursorStartPosition = start;
|
||||
|
||||
for (let j = 0; j < timesToRepeat; j++) {
|
||||
await this.exec(stop, vimState);
|
||||
}
|
||||
|
||||
resultingCursors.push(new Cursor(vimState.cursorStartPosition, vimState.cursorStopPosition));
|
||||
|
||||
for (const transformation of vimState.recordedState.transformer.transformations) {
|
||||
if (isTextTransformation(transformation) && transformation.cursorIndex === undefined) {
|
||||
transformation.cursorIndex = this.multicursorIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vimState.cursors = resultingCursors;
|
||||
}
|
||||
}
|
||||
|
||||
export enum KeypressState {
|
||||
WaitingOnKeys,
|
||||
NoPossibleMatch,
|
||||
}
|
||||
|
||||
/**
|
||||
* Every Vim action will be added here with the @RegisterAction decorator.
|
||||
*/
|
||||
const actionMap = new Map<Mode, Array<new () => BaseAction>>();
|
||||
|
||||
/**
|
||||
* Gets the action that should be triggered given a key sequence.
|
||||
*
|
||||
* If there is a definitive action that matched, returns that action.
|
||||
*
|
||||
* If an action could potentially match if more keys were to be pressed, returns `KeyPressState.WaitingOnKeys`
|
||||
* (e.g. you pressed "g" and are about to press "g" action to make the full action "gg")
|
||||
*
|
||||
* If no action could ever match, returns `KeypressState.NoPossibleMatch`.
|
||||
*/
|
||||
export function getRelevantAction(
|
||||
keysPressed: string[],
|
||||
vimState: VimState,
|
||||
): BaseAction | KeypressState {
|
||||
const possibleActionsForMode = actionMap.get(vimState.currentMode) ?? [];
|
||||
|
||||
let hasPotentialMatch = false;
|
||||
for (const actionType of possibleActionsForMode) {
|
||||
// TODO: Constructing up to several hundred Actions every time we hit a key is moronic.
|
||||
// I think we can make `doesActionApply` and `couldActionApply` static...
|
||||
const action = new actionType();
|
||||
if (action.doesActionApply(vimState, keysPressed)) {
|
||||
action.keysPressed = vimState.recordedState.actionKeys.slice(0);
|
||||
return action;
|
||||
}
|
||||
|
||||
hasPotentialMatch ||= action.couldActionApply(vimState, keysPressed);
|
||||
}
|
||||
|
||||
return hasPotentialMatch ? KeypressState.WaitingOnKeys : KeypressState.NoPossibleMatch;
|
||||
}
|
||||
|
||||
export function RegisterAction(action: new () => BaseAction): void {
|
||||
const actionInstance = new action();
|
||||
for (const modeName of actionInstance.modes) {
|
||||
let actions = actionMap.get(modeName);
|
||||
if (!actions) {
|
||||
actions = [];
|
||||
actionMap.set(modeName, actions);
|
||||
}
|
||||
|
||||
if (actionInstance.keys === undefined) {
|
||||
// action that can't be called directly
|
||||
continue;
|
||||
}
|
||||
|
||||
actions.push(action);
|
||||
}
|
||||
}
|
163
src/actions/baseMotion.ts
Normal file
163
src/actions/baseMotion.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import { BaseAction } from './base';
|
||||
import { Mode } from '../mode/mode';
|
||||
import { VimState } from '../state/vimState';
|
||||
import { clamp } from '../util/util';
|
||||
import { Position } from 'vscode';
|
||||
|
||||
export function isIMovement(o: IMovement | Position): o is IMovement {
|
||||
return (o as IMovement).start !== undefined && (o as IMovement).stop !== undefined;
|
||||
}
|
||||
|
||||
export enum SelectionType {
|
||||
Concatenating, // Selections that concatenate repeated movements
|
||||
Expanding, // Selections that expand the start and end of the previous selection
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a (more sophisticated) Movement.
|
||||
*/
|
||||
export interface IMovement {
|
||||
start: Position;
|
||||
stop: Position;
|
||||
|
||||
/**
|
||||
* Whether this motion succeeded. Some commands, like fx when 'x' can't be found,
|
||||
* will not move the cursor. Furthermore, dfx won't delete *anything*, even though
|
||||
* deleting to the current character would generally delete 1 character.
|
||||
*/
|
||||
failed?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this motion resulted in the current multicursor index being removed.
|
||||
* This happens when multiple selections combine into one.
|
||||
*/
|
||||
removed?: boolean;
|
||||
}
|
||||
|
||||
export function failedMovement(vimState: VimState): IMovement {
|
||||
return {
|
||||
start: vimState.cursorStartPosition,
|
||||
stop: vimState.cursorStopPosition,
|
||||
failed: true,
|
||||
};
|
||||
}
|
||||
|
||||
export abstract class BaseMovement extends BaseAction {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
|
||||
override actionType = 'motion' as const;
|
||||
|
||||
/**
|
||||
* If movement can be repeated with semicolon or comma this will be true when
|
||||
* running the repetition.
|
||||
*/
|
||||
isRepeat = false;
|
||||
|
||||
/**
|
||||
* This is for commands like $ which force the desired column to be at
|
||||
* the end of even the longest line.
|
||||
*/
|
||||
public setsDesiredColumnToEOL = false;
|
||||
|
||||
protected selectionType = SelectionType.Concatenating;
|
||||
|
||||
constructor(keysPressed?: string[], isRepeat?: boolean) {
|
||||
super();
|
||||
|
||||
if (keysPressed) {
|
||||
this.keysPressed = keysPressed;
|
||||
}
|
||||
|
||||
if (isRepeat) {
|
||||
this.isRepeat = isRepeat;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the movement a single time.
|
||||
*
|
||||
* Generally returns a new Position. If necessary, it can return an IMovement instead.
|
||||
* Note: If returning an IMovement, make sure that repeated actions on a
|
||||
* visual selection work. For example, V}}
|
||||
*/
|
||||
public async execAction(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
firstIteration: boolean,
|
||||
lastIteration: boolean,
|
||||
): Promise<Position | IMovement> {
|
||||
throw new Error('Not implemented!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the movement in an operator context a single time.
|
||||
*
|
||||
* Some movements operate over different ranges when used for operators.
|
||||
*/
|
||||
protected async execActionForOperator(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
firstIteration: boolean,
|
||||
lastIteration: boolean,
|
||||
): Promise<Position | IMovement> {
|
||||
return this.execAction(position, vimState, firstIteration, lastIteration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a movement count times.
|
||||
*
|
||||
* count: the number prefix the user entered, or 0 if they didn't enter one.
|
||||
*/
|
||||
public async execActionWithCount(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
count: number,
|
||||
): Promise<Position | IMovement> {
|
||||
let result!: Position | IMovement;
|
||||
let prevResult = failedMovement(vimState);
|
||||
let firstMovementStart = position;
|
||||
|
||||
count = clamp(count, 1, 99999);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const firstIteration = i === 0;
|
||||
const lastIteration = i === count - 1;
|
||||
result =
|
||||
vimState.recordedState.operator && lastIteration
|
||||
? await this.execActionForOperator(position, vimState, firstIteration, lastIteration)
|
||||
: await this.execAction(position, vimState, firstIteration, lastIteration);
|
||||
|
||||
if (result instanceof Position) {
|
||||
/**
|
||||
* This position will be passed to the `motion` on the next iteration,
|
||||
* it may cause some issues when count > 1.
|
||||
*/
|
||||
position = result;
|
||||
} else {
|
||||
if (result.failed) {
|
||||
return prevResult;
|
||||
}
|
||||
|
||||
if (firstIteration) {
|
||||
firstMovementStart = result.start;
|
||||
}
|
||||
|
||||
position = this.adjustPosition(position, result, lastIteration);
|
||||
prevResult = result;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selectionType === SelectionType.Concatenating && isIMovement(result)) {
|
||||
result.start = firstMovementStart;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected adjustPosition(position: Position, result: IMovement, lastIteration: boolean) {
|
||||
if (!lastIteration) {
|
||||
position = result.stop.getRightThroughLineBreaks();
|
||||
}
|
||||
return position;
|
||||
}
|
||||
}
|
2688
src/actions/commands/actions.ts
Normal file
2688
src/actions/commands/actions.ts
Normal file
File diff suppressed because it is too large
Load Diff
428
src/actions/commands/commandLine.ts
Normal file
428
src/actions/commands/commandLine.ts
Normal file
@ -0,0 +1,428 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { CommandLine, ExCommandLine, SearchCommandLine } from '../../cmd_line/commandLine';
|
||||
import { ErrorCode, VimError } from '../../error';
|
||||
import { Mode } from '../../mode/mode';
|
||||
import { Register, RegisterMode } from '../../register/register';
|
||||
import { RecordedState } from '../../state/recordedState';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { TextEditor } from '../../textEditor';
|
||||
import { Clipboard } from '../../util/clipboard';
|
||||
import { getPathDetails, readDirectory } from '../../util/path';
|
||||
import { builtinExCommands } from '../../vimscript/exCommandParser';
|
||||
import { SearchDirection } from '../../vimscript/pattern';
|
||||
import { BaseCommand, RegisterAction } from '../base';
|
||||
|
||||
abstract class CommandLineAction extends BaseCommand {
|
||||
modes = [Mode.CommandlineInProgress, Mode.SearchInProgressMode];
|
||||
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected abstract run(vimState: VimState, commandLine: CommandLine): Promise<void>;
|
||||
|
||||
public override async exec(position: vscode.Position, vimState: VimState): Promise<void> {
|
||||
if (
|
||||
!(
|
||||
vimState.modeData.mode === Mode.CommandlineInProgress ||
|
||||
vimState.modeData.mode === Mode.SearchInProgressMode
|
||||
)
|
||||
) {
|
||||
throw new Error(`Unexpected mode ${vimState.modeData.mode} in CommandLineAction`);
|
||||
}
|
||||
|
||||
await this.run(vimState, vimState.modeData.commandLine);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineTab extends CommandLineAction {
|
||||
override modes = [Mode.CommandlineInProgress];
|
||||
keys = [['<tab>'], ['<S-tab>']];
|
||||
|
||||
private cycleCompletion(isTabForward: boolean, commandLine: ExCommandLine) {
|
||||
const autoCompleteItems = commandLine.autoCompleteItems;
|
||||
if (autoCompleteItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
commandLine.autoCompleteIndex = isTabForward
|
||||
? (commandLine.autoCompleteIndex + 1) % autoCompleteItems.length
|
||||
: (commandLine.autoCompleteIndex - 1 + autoCompleteItems.length) % autoCompleteItems.length;
|
||||
|
||||
const lastPos = commandLine.preCompleteCharacterPos;
|
||||
const lastCmd = commandLine.preCompleteCommand;
|
||||
const evalCmd = lastCmd.slice(0, lastPos);
|
||||
const restCmd = lastCmd.slice(lastPos);
|
||||
|
||||
commandLine.text = evalCmd + autoCompleteItems[commandLine.autoCompleteIndex] + restCmd;
|
||||
commandLine.cursorIndex = commandLine.text.length - restCmd.length;
|
||||
}
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
if (!(commandLine instanceof ExCommandLine)) {
|
||||
throw new Error('Expected ExCommandLine in CommandLineTab::run()');
|
||||
}
|
||||
|
||||
const key = this.keysPressed[0];
|
||||
const isTabForward = key === '<tab>';
|
||||
|
||||
// If we hit <Tab> twice in a row, definitely cycle
|
||||
if (
|
||||
commandLine.autoCompleteItems.length !== 0 &&
|
||||
vimState.recordedState.actionsRun[vimState.recordedState.actionsRun.length - 2] instanceof
|
||||
CommandLineTab
|
||||
) {
|
||||
this.cycleCompletion(isTabForward, commandLine);
|
||||
return;
|
||||
}
|
||||
|
||||
let newCompletionItems: string[] = [];
|
||||
|
||||
// Sub string since vim does completion before the cursor
|
||||
let evalCmd = commandLine.text.slice(0, commandLine.cursorIndex);
|
||||
const restCmd = commandLine.text.slice(commandLine.cursorIndex);
|
||||
|
||||
// \s* is the match the extra space before any character like ': edit'
|
||||
const cmdRegex = /^\s*\w+$/;
|
||||
const fileRegex = /^\s*\w+\s+/g;
|
||||
if (cmdRegex.test(evalCmd)) {
|
||||
// Command completion
|
||||
newCompletionItems = builtinExCommands
|
||||
.map((pair) => pair[0][0] + pair[0][1])
|
||||
.filter((cmd) => cmd.startsWith(evalCmd))
|
||||
// Remove the already typed portion in the array
|
||||
.map((cmd) => cmd.slice(cmd.search(evalCmd) + evalCmd.length))
|
||||
.sort();
|
||||
} else if (fileRegex.exec(evalCmd)) {
|
||||
// File completion by searching if there is a space after the first word/command
|
||||
// ideally it should be a process of white-listing to selected commands like :e and :vsp
|
||||
const filePathInCmd = evalCmd.substring(fileRegex.lastIndex);
|
||||
const currentUri = vimState.document.uri;
|
||||
const isRemote = !!vscode.env.remoteName;
|
||||
|
||||
const {
|
||||
fullDirPath,
|
||||
baseName,
|
||||
partialPath,
|
||||
path: p,
|
||||
} = getPathDetails(filePathInCmd, currentUri, isRemote);
|
||||
// Update the evalCmd in case of windows, where we change / to \
|
||||
evalCmd = evalCmd.slice(0, fileRegex.lastIndex) + partialPath;
|
||||
|
||||
// test if the baseName is . or ..
|
||||
const shouldAddDotItems = /^\.\.?$/g.test(baseName);
|
||||
const dirItems = await readDirectory(
|
||||
fullDirPath,
|
||||
p.sep,
|
||||
currentUri,
|
||||
isRemote,
|
||||
shouldAddDotItems,
|
||||
);
|
||||
const startWithBaseNameRegex = new RegExp(
|
||||
`^${baseName}`,
|
||||
process.platform === 'win32' ? 'i' : '',
|
||||
);
|
||||
newCompletionItems = dirItems
|
||||
.map((name): [RegExpExecArray | null, string] => [startWithBaseNameRegex.exec(name), name])
|
||||
.filter(([isMatch]) => isMatch !== null)
|
||||
.map(([match, name]) => name.slice(match![0].length))
|
||||
.sort();
|
||||
}
|
||||
|
||||
const newIndex = isTabForward ? 0 : newCompletionItems.length - 1;
|
||||
commandLine.autoCompleteIndex = newIndex;
|
||||
// If here only one items we fill cmd direct, so the next tab will not cycle the one item array
|
||||
commandLine.autoCompleteItems = newCompletionItems.length <= 1 ? [] : newCompletionItems;
|
||||
commandLine.preCompleteCharacterPos = commandLine.cursorIndex;
|
||||
commandLine.preCompleteCommand = evalCmd + restCmd;
|
||||
|
||||
const completion = newCompletionItems.length === 0 ? '' : newCompletionItems[newIndex];
|
||||
commandLine.text = evalCmd + completion + restCmd;
|
||||
commandLine.cursorIndex = commandLine.text.length - restCmd.length;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class ExCommandLineEnter extends CommandLineAction {
|
||||
override modes = [Mode.CommandlineInProgress];
|
||||
keys = [['\n'], ['<C-m>']];
|
||||
|
||||
protected override async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.run(vimState);
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class SearchCommandLineEnter extends CommandLineAction {
|
||||
override modes = [Mode.SearchInProgressMode];
|
||||
keys = [['\n'], ['<C-m>']];
|
||||
|
||||
override runsOnceForEveryCursor() {
|
||||
return true;
|
||||
}
|
||||
override isJump = true;
|
||||
|
||||
protected override async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.run(vimState);
|
||||
if (this.multicursorIndex === vimState.cursors.length - 1) {
|
||||
// TODO: gah, this is stupid
|
||||
await vimState.setCurrentMode(commandLine.previousMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineEscape extends CommandLineAction {
|
||||
keys = [['<Esc>'], ['<C-c>'], ['<C-[>']];
|
||||
|
||||
protected override async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.escape(vimState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineCtrlF extends CommandLineAction {
|
||||
keys = ['<C-f>'];
|
||||
|
||||
protected override async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.ctrlF(vimState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineBackspace extends CommandLineAction {
|
||||
keys = [['<BS>'], ['<S-BS>'], ['<C-h>']];
|
||||
|
||||
protected override async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.backspace(vimState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineDelete extends CommandLineAction {
|
||||
keys = ['<Del>'];
|
||||
|
||||
protected override async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.delete(vimState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandlineHome extends CommandLineAction {
|
||||
keys = [['<Home>'], ['<C-b>']];
|
||||
|
||||
protected override async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.home();
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineEnd extends CommandLineAction {
|
||||
keys = [['<End>'], ['<C-e>']];
|
||||
|
||||
protected override async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.end();
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineDeleteWord extends CommandLineAction {
|
||||
keys = [['<C-w>'], ['<C-BS>']];
|
||||
|
||||
protected override async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.deleteWord();
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineDeleteToBeginning extends CommandLineAction {
|
||||
keys = ['<C-u>'];
|
||||
|
||||
protected override async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.deleteToBeginning();
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineWordLeft extends CommandLineAction {
|
||||
keys = ['<C-left>'];
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.wordLeft();
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineWordRight extends CommandLineAction {
|
||||
keys = ['<C-right>'];
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.wordRight();
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineHistoryBack extends CommandLineAction {
|
||||
keys = [['<up>'], ['<C-p>']];
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.historyBack();
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineHistoryForward extends CommandLineAction {
|
||||
keys = [['<down>'], ['<C-n>']];
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
await commandLine.historyForward();
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandInsertRegisterContentInCommandLine extends CommandLineAction {
|
||||
keys = ['<C-r>', '<character>'];
|
||||
override isCompleteAction = false;
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
if (!Register.isValidRegister(this.keysPressed[1])) {
|
||||
return;
|
||||
}
|
||||
|
||||
vimState.recordedState.registerName = this.keysPressed[1];
|
||||
const register = await Register.get(vimState.recordedState.registerName, this.multicursorIndex);
|
||||
if (register === undefined) {
|
||||
StatusBar.displayError(
|
||||
vimState,
|
||||
VimError.fromCode(ErrorCode.NothingInRegister, vimState.recordedState.registerName),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let text: string;
|
||||
if (register.text instanceof Array) {
|
||||
text = register.text.join('\n');
|
||||
} else if (register.text instanceof RecordedState) {
|
||||
let keyStrokes: string[] = [];
|
||||
|
||||
for (const action of register.text.actionsRun) {
|
||||
keyStrokes = keyStrokes.concat(action.keysPressed);
|
||||
}
|
||||
|
||||
text = keyStrokes.join('\n');
|
||||
} else {
|
||||
text = register.text;
|
||||
}
|
||||
|
||||
if (register.registerMode === RegisterMode.LineWise) {
|
||||
text += '\n';
|
||||
}
|
||||
|
||||
commandLine.text += text;
|
||||
commandLine.cursorIndex += text.length;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandInsertWord extends CommandLineAction {
|
||||
keys = ['<C-r>', '<C-w>'];
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
const word = TextEditor.getWord(vimState.document, vimState.cursorStopPosition.getLeftIfEOL());
|
||||
|
||||
if (word !== undefined) {
|
||||
commandLine.text += word;
|
||||
commandLine.cursorIndex += word.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineLeftRight extends CommandLineAction {
|
||||
keys = [['<left>'], ['<right>']];
|
||||
|
||||
private getTrimmedStatusBarText() {
|
||||
// first regex removes the : / and | from the string
|
||||
// second regex removes a single space from the end of the string
|
||||
const trimmedStatusBarText = StatusBar.getText()
|
||||
.replace(/^(?:\/|\:)(.*)(?:\|)(.*)/, '$1$2')
|
||||
.replace(/(.*) $/, '$1');
|
||||
return trimmedStatusBarText;
|
||||
}
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
const key = this.keysPressed[0];
|
||||
const statusBarText = this.getTrimmedStatusBarText();
|
||||
if (key === '<right>') {
|
||||
commandLine.cursorIndex = Math.min(commandLine.cursorIndex + 1, statusBarText.length);
|
||||
} else if (key === '<left>') {
|
||||
commandLine.cursorIndex = Math.max(commandLine.cursorIndex - 1, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLinePaste extends CommandLineAction {
|
||||
keys = [['<C-v>'], ['<D-v>']];
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
const textFromClipboard = await Clipboard.Paste();
|
||||
|
||||
commandLine.text = commandLine.text
|
||||
.substring(0, commandLine.cursorIndex)
|
||||
.concat(textFromClipboard)
|
||||
.concat(commandLine.text.slice(commandLine.cursorIndex));
|
||||
commandLine.cursorIndex += textFromClipboard.length;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCtrlLInSearchMode extends CommandLineAction {
|
||||
override modes = [Mode.SearchInProgressMode];
|
||||
keys = ['<C-l>'];
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
if (commandLine instanceof SearchCommandLine) {
|
||||
const currentMatch = commandLine.getCurrentMatchRange(vimState);
|
||||
if (currentMatch) {
|
||||
const line = vimState.document.lineAt(currentMatch.range.end).text;
|
||||
if (currentMatch.range.end.character < line.length) {
|
||||
commandLine.getSearchState().searchString += line[currentMatch.range.end.character];
|
||||
commandLine.cursorIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandAdvanceCurrentMatch extends CommandLineAction {
|
||||
override modes = [Mode.SearchInProgressMode];
|
||||
keys = [['<C-g>'], ['<C-t>']];
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
const key = this.keysPressed[0];
|
||||
const direction =
|
||||
key === '<C-g>'
|
||||
? SearchDirection.Forward
|
||||
: key === '<C-t>'
|
||||
? SearchDirection.Backward
|
||||
: undefined;
|
||||
if (commandLine instanceof SearchCommandLine && direction !== undefined) {
|
||||
void commandLine.advanceCurrentMatch(vimState, direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandLineType extends CommandLineAction {
|
||||
keys = [['<character>']];
|
||||
|
||||
protected async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
|
||||
void commandLine.typeCharacter(this.keysPressed[0]);
|
||||
}
|
||||
}
|
1366
src/actions/commands/digraphs.ts
Normal file
1366
src/actions/commands/digraphs.ts
Normal file
File diff suppressed because it is too large
Load Diff
110
src/actions/commands/fold.ts
Normal file
110
src/actions/commands/fold.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Position } from 'vscode';
|
||||
import { Cursor } from '../../common/motion/cursor';
|
||||
import { Mode } from '../../mode/mode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { BaseCommand, RegisterAction } from '../base';
|
||||
import { BaseOperator } from '../operator';
|
||||
|
||||
type FoldDirection = 'up' | 'down' | undefined;
|
||||
abstract class CommandFold extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
abstract commandName: string;
|
||||
direction: FoldDirection | undefined;
|
||||
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
// Don't run if there's an operator because the Sneak plugin uses <operator>z
|
||||
return (
|
||||
super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined
|
||||
);
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const timesToRepeat = vimState.recordedState.count || 1;
|
||||
const args =
|
||||
this.direction !== undefined
|
||||
? { levels: timesToRepeat, direction: this.direction }
|
||||
: undefined;
|
||||
vimState.recordedState.transformer.vscodeCommand(this.commandName, args);
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandToggleFold extends CommandFold {
|
||||
keys = ['z', 'a'];
|
||||
commandName = 'editor.toggleFold';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCloseFold extends CommandFold {
|
||||
keys = ['z', 'c'];
|
||||
commandName = 'editor.fold';
|
||||
override direction: FoldDirection = 'up';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCloseAllFolds extends CommandFold {
|
||||
keys = ['z', 'M'];
|
||||
commandName = 'editor.foldAll';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandOpenFold extends CommandFold {
|
||||
keys = ['z', 'o'];
|
||||
commandName = 'editor.unfold';
|
||||
override direction: FoldDirection = 'down';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandOpenAllFolds extends CommandFold {
|
||||
keys = ['z', 'R'];
|
||||
commandName = 'editor.unfoldAll';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCloseAllFoldsRecursively extends CommandFold {
|
||||
override modes = [Mode.Normal];
|
||||
keys = ['z', 'C'];
|
||||
commandName = 'editor.foldRecursively';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandOpenAllFoldsRecursively extends CommandFold {
|
||||
override modes = [Mode.Normal];
|
||||
keys = ['z', 'O'];
|
||||
commandName = 'editor.unfoldRecursively';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class AddFold extends BaseOperator {
|
||||
override modes = [Mode.Normal, Mode.Visual];
|
||||
keys = ['z', 'f'];
|
||||
|
||||
readonly commandName = 'editor.createFoldingRangeFromSelection';
|
||||
|
||||
public async run(vimState: VimState, start: Position, end: Position): Promise<void> {
|
||||
const previousSelections = vimState.lastVisualSelection; // keep in case of Normal mode
|
||||
vimState.editor.selection = new vscode.Selection(start, end);
|
||||
await vscode.commands.executeCommand(this.commandName);
|
||||
vimState.lastVisualSelection = previousSelections;
|
||||
vimState.cursors = [new Cursor(start, start)];
|
||||
await vimState.setCurrentMode(Mode.Normal); // Vim behavior
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class RemoveFold extends BaseCommand {
|
||||
override modes = [Mode.Normal, Mode.Visual];
|
||||
keys = ['z', 'd'];
|
||||
readonly commandName = 'editor.removeManualFoldingRanges';
|
||||
|
||||
override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
await vscode.commands.executeCommand(this.commandName);
|
||||
|
||||
const newCursorPosition =
|
||||
vimState.currentMode === Mode.Visual ? vimState.editor.selection.start : position;
|
||||
vimState.cursors = [new Cursor(newCursorPosition, newCursorPosition)];
|
||||
await vimState.setCurrentMode(Mode.Normal); // Vim behavior
|
||||
}
|
||||
}
|
557
src/actions/commands/insert.ts
Normal file
557
src/actions/commands/insert.ts
Normal file
@ -0,0 +1,557 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { Position } from 'vscode';
|
||||
import { lineCompletionProvider } from '../../completion/lineCompletionProvider';
|
||||
import { ErrorCode, VimError } from '../../error';
|
||||
import { RecordedState } from '../../state/recordedState';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { isHighSurrogate, isLowSurrogate } from '../../util/util';
|
||||
import { PositionDiff } from './../../common/motion/position';
|
||||
import { configuration } from './../../configuration/configuration';
|
||||
import { Mode } from './../../mode/mode';
|
||||
import { Register, RegisterMode } from './../../register/register';
|
||||
import { TextEditor } from './../../textEditor';
|
||||
import { BaseCommand, RegisterAction } from './../base';
|
||||
import { ArrowsInInsertMode } from './../motion';
|
||||
import {
|
||||
CommandInsertAfterCursor,
|
||||
CommandInsertAtCursor,
|
||||
CommandInsertAtFirstCharacter,
|
||||
CommandInsertAtLastChange,
|
||||
CommandInsertAtLineBegin,
|
||||
CommandInsertAtLineEnd,
|
||||
CommandInsertNewLineAbove,
|
||||
CommandInsertNewLineBefore,
|
||||
CommandReplaceAtCursorFromNormalMode,
|
||||
DocumentContentChangeAction,
|
||||
} from './actions';
|
||||
import { DefaultDigraphs } from './digraphs';
|
||||
|
||||
@RegisterAction
|
||||
export class CommandEscInsertMode extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = [['<Esc>'], ['<C-c>'], ['<C-[>']];
|
||||
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
void vscode.commands.executeCommand('closeParameterHints');
|
||||
|
||||
vimState.cursors = vimState.cursors.map((x) => x.withNewStop(x.stop.getLeft()));
|
||||
if (vimState.returnToInsertAfterCommand && position.character !== 0) {
|
||||
vimState.cursors = vimState.cursors.map((x) => x.withNewStop(x.stop.getRight()));
|
||||
}
|
||||
|
||||
// only remove leading spaces inserted by vscode.
|
||||
// vscode only inserts them when user enter a new line,
|
||||
// ie, o/O in Normal mode or \n in Insert mode.
|
||||
const lastActionBeforeEsc =
|
||||
vimState.recordedState.actionsRun[vimState.recordedState.actionsRun.length - 2];
|
||||
if (
|
||||
vimState.document.languageId !== 'plaintext' &&
|
||||
(lastActionBeforeEsc instanceof CommandInsertNewLineBefore ||
|
||||
lastActionBeforeEsc instanceof CommandInsertNewLineAbove ||
|
||||
(lastActionBeforeEsc instanceof DocumentContentChangeAction &&
|
||||
lastActionBeforeEsc.keysPressed[lastActionBeforeEsc.keysPressed.length - 1] === '\n'))
|
||||
) {
|
||||
for (const cursor of vimState.cursors) {
|
||||
const line = vimState.document.lineAt(cursor.stop);
|
||||
if (line.text.length > 0 && line.isEmptyOrWhitespace) {
|
||||
vimState.recordedState.transformer.delete(line.range);
|
||||
}
|
||||
}
|
||||
}
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
|
||||
// If we wanted to repeat this insert (only for i and a), now is the time to do it. Insert
|
||||
// count amount of these strings before returning back to normal mode
|
||||
const shouldRepeatInsert =
|
||||
vimState.recordedState.count > 1 &&
|
||||
vimState.recordedState.actionsRun.find(
|
||||
(a) =>
|
||||
a instanceof CommandInsertAtCursor ||
|
||||
a instanceof CommandInsertAfterCursor ||
|
||||
a instanceof CommandInsertAtLineBegin ||
|
||||
a instanceof CommandInsertAtLineEnd ||
|
||||
a instanceof CommandInsertAtFirstCharacter ||
|
||||
a instanceof CommandInsertAtLastChange,
|
||||
) !== undefined;
|
||||
|
||||
// If this is the type to repeat insert, do this now
|
||||
if (shouldRepeatInsert) {
|
||||
const changeAction = vimState.recordedState.actionsRun
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((a) => a instanceof DocumentContentChangeAction);
|
||||
if (changeAction instanceof DocumentContentChangeAction) {
|
||||
// Add count amount of inserts in the case of 4i=<esc>
|
||||
// TODO: A few actions such as <C-t> should be repeated, but are not
|
||||
for (let i = 0; i < vimState.recordedState.count - 1; i++) {
|
||||
// If this is the last transform, move cursor back one character
|
||||
const positionDiff =
|
||||
i === vimState.recordedState.count - 2
|
||||
? PositionDiff.offset({ character: -1 })
|
||||
: PositionDiff.identity();
|
||||
|
||||
// Add a transform containing the change
|
||||
vimState.recordedState.transformer.addTransformation(
|
||||
changeAction.getTransformation(positionDiff),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (vimState.historyTracker.currentContentChanges.length > 0) {
|
||||
vimState.historyTracker.currentContentChanges = [];
|
||||
}
|
||||
|
||||
if (vimState.isFakeMultiCursor) {
|
||||
vimState.cursors = [vimState.cursors[0]];
|
||||
vimState.isFakeMultiCursor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class CommandInsertPreviousText extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-a>'];
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const register = await Register.get('.');
|
||||
if (
|
||||
register === undefined ||
|
||||
!(register.text instanceof RecordedState) ||
|
||||
!register.text.actionsRun
|
||||
) {
|
||||
throw VimError.fromCode(ErrorCode.NoInsertedTextYet);
|
||||
}
|
||||
|
||||
const recordedState = register.text.clone();
|
||||
|
||||
// The first action is entering Insert Mode, which is not necessary in this case
|
||||
recordedState.actionsRun.shift();
|
||||
|
||||
// The last action is leaving Insert Mode, which is not necessary in this case
|
||||
recordedState.actionsRun.pop();
|
||||
|
||||
if (recordedState.actionsRun?.[0] instanceof ArrowsInInsertMode) {
|
||||
// Note, arrow keys are the only Insert action command that can't be repeated here as far as @rebornix knows.
|
||||
recordedState.actionsRun.shift();
|
||||
}
|
||||
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'replayRecordedState',
|
||||
recordedState,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandInsertPreviousTextAndQuit extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-shift+2>']; // <C-@>
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
await new CommandInsertPreviousText().exec(position, vimState);
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class IndentCommand extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
abstract readonly delta: number;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const line = vimState.document.lineAt(position);
|
||||
const tabSize = Number(vimState.editor.options.tabSize);
|
||||
const indentationWidth = TextEditor.getIndentationLevel(line.text, tabSize);
|
||||
const newIndentationWidth = (Math.floor(indentationWidth / tabSize) + this.delta) * tabSize;
|
||||
|
||||
vimState.recordedState.transformer.replace(
|
||||
new vscode.Range(
|
||||
position.getLineBegin(),
|
||||
position.with({ character: line.firstNonWhitespaceCharacterIndex }),
|
||||
),
|
||||
TextEditor.setIndentationLevel(
|
||||
line.text,
|
||||
newIndentationWidth,
|
||||
vimState.editor.options.insertSpaces as boolean,
|
||||
).match(/^(\s*)/)![1],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class IncreaseIndent extends IndentCommand {
|
||||
keys = ['<C-t>'];
|
||||
override readonly delta = 1;
|
||||
}
|
||||
@RegisterAction
|
||||
class DecreaseIndent extends IndentCommand {
|
||||
keys = ['<C-d>'];
|
||||
override readonly delta = -1;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class CommandBackspaceInInsertMode extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = [['<BS>'], ['<C-h>']];
|
||||
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.recordedState.transformer.vscodeCommand('deleteLeft');
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandDeleteInInsertMode extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<Del>'];
|
||||
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.recordedState.transformer.vscodeCommand('deleteRight');
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class CommandInsertInInsertMode extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<character>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const char = this.keysPressed[this.keysPressed.length - 1];
|
||||
|
||||
let text = char;
|
||||
|
||||
if (char.length === 1) {
|
||||
const prevHighSurrogate =
|
||||
vimState.modeData.mode === Mode.Insert ? vimState.modeData.highSurrogate : undefined;
|
||||
|
||||
if (isHighSurrogate(char.charCodeAt(0))) {
|
||||
await vimState.setModeData({
|
||||
mode: Mode.Insert,
|
||||
highSurrogate: char,
|
||||
});
|
||||
|
||||
if (prevHighSurrogate === undefined) return;
|
||||
text = prevHighSurrogate;
|
||||
} else {
|
||||
if (isLowSurrogate(char.charCodeAt(0)) && prevHighSurrogate !== undefined) {
|
||||
text = prevHighSurrogate + char;
|
||||
}
|
||||
|
||||
await vimState.setModeData({
|
||||
mode: Mode.Insert,
|
||||
highSurrogate: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'insertTextVSCode',
|
||||
text,
|
||||
isMultiCursor: vimState.isMultiCursor,
|
||||
});
|
||||
}
|
||||
|
||||
public override toString(): string {
|
||||
return this.keysPressed[this.keysPressed.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandInsertDigraph extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-k>', '<any>', '<any>'];
|
||||
override isCompleteAction = false;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const digraph = this.keysPressed.slice(1, 3).join('');
|
||||
const reverseDigraph = digraph.split('').reverse().join('');
|
||||
let charCodes = (DefaultDigraphs.get(digraph) ||
|
||||
DefaultDigraphs.get(reverseDigraph) ||
|
||||
configuration.digraphs[digraph] ||
|
||||
configuration.digraphs[reverseDigraph])[1];
|
||||
if (!(charCodes instanceof Array)) {
|
||||
charCodes = [charCodes];
|
||||
}
|
||||
const char = String.fromCharCode(...charCodes);
|
||||
vimState.recordedState.transformer.insert(position, char);
|
||||
}
|
||||
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
if (!super.doesActionApply(vimState, keysPressed)) {
|
||||
return false;
|
||||
}
|
||||
const chars = keysPressed.slice(1, 3).join('');
|
||||
const reverseChars = chars.split('').reverse().join('');
|
||||
return (
|
||||
chars in configuration.digraphs ||
|
||||
reverseChars in configuration.digraphs ||
|
||||
DefaultDigraphs.has(chars) ||
|
||||
DefaultDigraphs.has(reverseChars)
|
||||
);
|
||||
}
|
||||
|
||||
public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
if (!super.couldActionApply(vimState, keysPressed)) {
|
||||
return false;
|
||||
}
|
||||
const chars = keysPressed.slice(1, keysPressed.length).join('');
|
||||
const reverseChars = chars.split('').reverse().join('');
|
||||
if (chars.length > 0) {
|
||||
const predicate = (digraph: string) => {
|
||||
const digraphChars = digraph.substring(0, chars.length);
|
||||
return chars === digraphChars || reverseChars === digraphChars;
|
||||
};
|
||||
const match =
|
||||
Object.keys(configuration.digraphs).find(predicate) ||
|
||||
[...DefaultDigraphs.keys()].find(predicate);
|
||||
return match !== undefined;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandInsertRegisterContent extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-r>', '<character>'];
|
||||
override isCompleteAction = false;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
if (!Register.isValidRegister(this.keysPressed[1])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const register = await Register.get(this.keysPressed[1], this.multicursorIndex);
|
||||
if (register === undefined) {
|
||||
StatusBar.displayError(
|
||||
vimState,
|
||||
VimError.fromCode(ErrorCode.NothingInRegister, this.keysPressed[1]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (register.text instanceof RecordedState) {
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'macro',
|
||||
register: vimState.recordedState.registerName,
|
||||
replay: 'keystrokes',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let text = register.text;
|
||||
if (register.registerMode === RegisterMode.LineWise && !vimState.isMultiCursor) {
|
||||
text += '\n';
|
||||
}
|
||||
|
||||
vimState.recordedState.transformer.insert(position, text);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandOneNormalCommandInInsertMode extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-o>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.returnToInsertAfterCommand = true;
|
||||
vimState.actionCount = 0;
|
||||
await new CommandEscInsertMode().exec(position, vimState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCtrlW extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-w>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
if (position.isAtDocumentBegin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let wordBegin: Position;
|
||||
if (position.isInLeadingWhitespace(vimState.document)) {
|
||||
wordBegin = position.getLineBegin();
|
||||
} else if (position.isLineBeginning()) {
|
||||
wordBegin = position.getUp().getLineEnd();
|
||||
} else {
|
||||
wordBegin = position.prevWordStart(vimState.document);
|
||||
}
|
||||
|
||||
vimState.recordedState.transformer.delete(new vscode.Range(wordBegin, position));
|
||||
|
||||
vimState.cursorStopPosition = wordBegin;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class InsertCharAbove extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-y>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
if (position.line === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const charPos = position.getUp();
|
||||
if (charPos.isLineEnd()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char = vimState.document.getText(new vscode.Range(charPos, charPos.getRight()));
|
||||
|
||||
vimState.recordedState.transformer.insert(position, char);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class InsertCharBelow extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-e>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
if (position.line >= vimState.document.lineCount - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const charPos = position.getDown();
|
||||
if (charPos.isLineEnd()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char = vimState.document.getText(new vscode.Range(charPos, charPos.getRight()));
|
||||
|
||||
vimState.recordedState.transformer.insert(position, char);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCtrlUInInsertMode extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-u>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
let start: Position;
|
||||
if (position.character === 0) {
|
||||
start = position.getLeftThroughLineBreaks(true);
|
||||
} else if (position.isInLeadingWhitespace(vimState.document)) {
|
||||
start = position.getLineBegin();
|
||||
} else {
|
||||
start = position.getLineBeginRespectingIndent(vimState.document);
|
||||
}
|
||||
|
||||
vimState.recordedState.transformer.delete(new vscode.Range(start, position));
|
||||
|
||||
vimState.cursorStopPosition = start;
|
||||
vimState.cursorStartPosition = start;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandNavigateAutocompleteDown extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = [['<C-n>'], ['<C-j>']];
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
await vscode.commands.executeCommand('selectNextSuggestion');
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandNavigateAutocompleteUp extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-p>'];
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
await vscode.commands.executeCommand('selectPrevSuggestion');
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCtrlVInInsertMode extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-v>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const clipboard = await Register.get('*', this.multicursorIndex);
|
||||
const text = clipboard?.text instanceof RecordedState ? undefined : clipboard?.text;
|
||||
|
||||
if (text) {
|
||||
vimState.recordedState.transformer.insert(vimState.cursorStopPosition, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandShowLineAutocomplete extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-x>', '<C-l>'];
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
await lineCompletionProvider.showLineCompletionsQuickPick(position, vimState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class NewLineInsertMode extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = [['<C-j>'], ['<C-m>']];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.recordedState.transformer.insert(
|
||||
position,
|
||||
'\n',
|
||||
PositionDiff.offset({ character: -1 }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandReplaceAtCursorFromInsertMode extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<Insert>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
await new CommandReplaceAtCursorFromNormalMode().exec(position, vimState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CreateUndoPoint extends BaseCommand {
|
||||
modes = [Mode.Insert];
|
||||
keys = ['<C-g>', 'u'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.historyTracker.addChange(true);
|
||||
vimState.historyTracker.finishCurrentStep();
|
||||
}
|
||||
}
|
618
src/actions/commands/put.ts
Normal file
618
src/actions/commands/put.ts
Normal file
@ -0,0 +1,618 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Position, TextDocument } from 'vscode';
|
||||
import { laterOf, PositionDiff, sorted } from '../../common/motion/position';
|
||||
import { configuration } from '../../configuration/configuration';
|
||||
import { isVisualMode, Mode } from '../../mode/mode';
|
||||
import { Register, RegisterMode, IRegisterContent } from '../../register/register';
|
||||
import { RecordedState } from '../../state/recordedState';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { TextEditor } from '../../textEditor';
|
||||
import { reportLinesChanged } from '../../util/statusBarTextUtils';
|
||||
import { BaseCommand, RegisterAction } from '../base';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { VimError, ErrorCode } from '../../error';
|
||||
import { Cursor } from '../../common/motion/cursor';
|
||||
import { Transformation } from '../../transformations/transformations';
|
||||
|
||||
function firstNonBlankChar(text: string): number {
|
||||
return text.match(/\S/)?.index ?? 0;
|
||||
}
|
||||
|
||||
type GetCursorPositionParams = {
|
||||
document: TextDocument;
|
||||
mode: Mode;
|
||||
replaceRange: vscode.Range;
|
||||
registerMode: RegisterMode;
|
||||
count: number;
|
||||
text: string;
|
||||
returnToInsertAfterCommand: boolean;
|
||||
};
|
||||
|
||||
abstract class BasePutCommand extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
override createsUndoPoint = true;
|
||||
|
||||
protected overwritesRegisterWithSelection = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const register = await Register.get(vimState.recordedState.registerName, this.multicursorIndex);
|
||||
if (register === undefined) {
|
||||
StatusBar.displayError(
|
||||
vimState,
|
||||
VimError.fromCode(ErrorCode.NothingInRegister, vimState.recordedState.registerName),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const count = vimState.recordedState.count || 1;
|
||||
|
||||
const mode =
|
||||
vimState.currentMode === Mode.CommandlineInProgress ? Mode.Normal : vimState.currentMode;
|
||||
const registerMode = this.getRegisterMode(register);
|
||||
|
||||
const replaceRange = this.getReplaceRange(mode, vimState.cursors[0], registerMode);
|
||||
|
||||
let text = this.getRegisterText(mode, register, count);
|
||||
if (this.shouldAdjustIndent(mode, registerMode)) {
|
||||
let lineToMatch: number | undefined;
|
||||
if (mode === Mode.VisualLine) {
|
||||
const [start, end] = sorted(vimState.cursorStartPosition, vimState.cursorStopPosition);
|
||||
if (end.line < vimState.document.lineCount - 1) {
|
||||
lineToMatch = end.line + 1;
|
||||
} else if (start.line > 0) {
|
||||
lineToMatch = start.line - 1;
|
||||
}
|
||||
} else {
|
||||
lineToMatch = position.line;
|
||||
}
|
||||
text = this.adjustIndent(
|
||||
lineToMatch !== undefined ? vimState.document.lineAt(lineToMatch).text : '',
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
const newCursorPosition = this.getCursorPosition({
|
||||
document: vimState.document,
|
||||
returnToInsertAfterCommand: vimState.returnToInsertAfterCommand,
|
||||
mode,
|
||||
replaceRange,
|
||||
registerMode,
|
||||
count,
|
||||
text,
|
||||
});
|
||||
|
||||
vimState.recordedState.transformer.moveCursor(
|
||||
PositionDiff.exactPosition(newCursorPosition),
|
||||
this.multicursorIndex ?? 0,
|
||||
);
|
||||
|
||||
if (registerMode === RegisterMode.LineWise) {
|
||||
text = this.adjustLinewiseRegisterText(mode, text);
|
||||
}
|
||||
|
||||
for (const transformation of this.getTransformations(
|
||||
vimState.document,
|
||||
mode,
|
||||
replaceRange,
|
||||
registerMode,
|
||||
text,
|
||||
)) {
|
||||
vimState.recordedState.transformer.addTransformation(transformation);
|
||||
}
|
||||
|
||||
// We do not run this in multi-cursor mode as it will overwrite the register for upcoming put iterations
|
||||
if (isVisualMode(mode) && !vimState.isMultiCursor) {
|
||||
// After using "p" or "P" in Visual mode the text that was put will be selected (from Vim's ":help gv").
|
||||
vimState.lastVisualSelection = {
|
||||
mode,
|
||||
start: replaceRange.start,
|
||||
end: replaceRange.start.advancePositionByText(text),
|
||||
};
|
||||
|
||||
if (this.overwritesRegisterWithSelection) {
|
||||
vimState.recordedState.registerName = configuration.useSystemClipboard ? '*' : '"';
|
||||
Register.put(
|
||||
vimState,
|
||||
vimState.document.getText(replaceRange),
|
||||
this.multicursorIndex,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Report lines changed
|
||||
let numNewlinesAfterPut = text.split('\n').length;
|
||||
if (registerMode === RegisterMode.LineWise) {
|
||||
numNewlinesAfterPut--;
|
||||
}
|
||||
reportLinesChanged(numNewlinesAfterPut, vimState);
|
||||
|
||||
const isLastCursor =
|
||||
!vimState.isMultiCursor || vimState.cursors.length - 1 === this.multicursorIndex;
|
||||
// Place the cursor back into normal mode after all puts are completed
|
||||
if (isLastCursor) {
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
}
|
||||
}
|
||||
|
||||
private getRegisterText(mode: Mode, register: IRegisterContent, count: number): string {
|
||||
if (register.text instanceof RecordedState) {
|
||||
return register.text.actionsRun
|
||||
.map((action) => action.keysPressed.join(''))
|
||||
.join('')
|
||||
.repeat(count);
|
||||
}
|
||||
|
||||
if (register.registerMode === RegisterMode.CharacterWise) {
|
||||
return mode === Mode.VisualLine
|
||||
? Array(count).fill(register.text).join('\n')
|
||||
: register.text.repeat(count);
|
||||
} else if (register.registerMode === RegisterMode.LineWise || mode === Mode.VisualLine) {
|
||||
return Array(count).fill(register.text).join('\n');
|
||||
} else if (register.registerMode === RegisterMode.BlockWise) {
|
||||
const lines = register.text.split('\n');
|
||||
const longestLength = Math.max(...lines.map((line) => line.length));
|
||||
return lines
|
||||
.map((line) => {
|
||||
const space = longestLength - line.length;
|
||||
const lineWithSpace = line + ' '.repeat(space);
|
||||
return lineWithSpace.repeat(count - 1) + line;
|
||||
})
|
||||
.join('\n');
|
||||
} else {
|
||||
throw new Error(`Unexpected RegisterMode ${register.registerMode}`);
|
||||
}
|
||||
}
|
||||
|
||||
private adjustIndent(lineToMatch: string, text: string): string {
|
||||
const lines = text.split('\n');
|
||||
|
||||
// Adjust indent to current line
|
||||
const tabSize = configuration.tabstop; // TODO: Use `editor.options.tabSize`, I think
|
||||
const indentationWidth = TextEditor.getIndentationLevel(lineToMatch, tabSize);
|
||||
const firstLineIdentationWidth = TextEditor.getIndentationLevel(lines[0], tabSize);
|
||||
|
||||
return lines
|
||||
.map((line) => {
|
||||
const currentIdentationWidth = TextEditor.getIndentationLevel(line, tabSize);
|
||||
const newIndentationWidth =
|
||||
currentIdentationWidth - firstLineIdentationWidth + indentationWidth;
|
||||
|
||||
// TODO: Use `editor.options.insertSpaces`, I think
|
||||
return TextEditor.setIndentationLevel(line, newIndentationWidth, configuration.expandtab);
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
private getTransformations(
|
||||
document: TextDocument,
|
||||
mode: Mode,
|
||||
replaceRange: vscode.Range,
|
||||
registerMode: RegisterMode,
|
||||
text: string,
|
||||
): Transformation[] {
|
||||
// Pasting block-wise content is very different, except in VisualLine mode, where it works exactly like line-wise
|
||||
if (registerMode === RegisterMode.BlockWise && mode !== Mode.VisualLine) {
|
||||
const transformations: Transformation[] = [];
|
||||
const lines = text.split('\n');
|
||||
const lineCount = Math.max(lines.length, replaceRange.end.line - replaceRange.start.line + 1);
|
||||
const longestLength = Math.max(...lines.map((line) => line.length));
|
||||
|
||||
// Only relevant for Visual mode
|
||||
// If we replace 2 newlines, subsequent transformations need to take that into account (otherwise we get overlaps)
|
||||
let deletedNewlines = 0;
|
||||
|
||||
for (let idx = 0; idx < lineCount; idx++) {
|
||||
const lineText = lines[idx] ?? '';
|
||||
|
||||
let range: vscode.Range;
|
||||
if (mode === Mode.VisualBlock) {
|
||||
if (replaceRange.start.line + idx > replaceRange.end.line) {
|
||||
const pos = replaceRange.start.with({ line: replaceRange.start.line + idx });
|
||||
range = new vscode.Range(pos, pos);
|
||||
} else {
|
||||
range = new vscode.Range(
|
||||
replaceRange.start.with({ line: replaceRange.start.line + idx }),
|
||||
replaceRange.end.with({ line: replaceRange.start.line + idx }),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (idx > 0) {
|
||||
const pos = replaceRange.start.with({
|
||||
line: replaceRange.start.line + idx + deletedNewlines,
|
||||
});
|
||||
range = new vscode.Range(pos, pos);
|
||||
} else {
|
||||
range = new vscode.Range(replaceRange.start, replaceRange.end);
|
||||
deletedNewlines = document.getText(range).split('\n').length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
const lineNumber = replaceRange.start.line + idx;
|
||||
if (lineNumber > document.lineCount - 1) {
|
||||
transformations.push({
|
||||
type: 'replaceText',
|
||||
range,
|
||||
text: '\n' + ' '.repeat(replaceRange.start.character) + lineText,
|
||||
});
|
||||
} else {
|
||||
const lineLength = document.lineAt(lineNumber).text.length;
|
||||
const leftPadding = Math.max(replaceRange.start.character - lineLength, 0);
|
||||
let rightPadding = 0;
|
||||
if (
|
||||
mode !== Mode.VisualBlock &&
|
||||
((lineNumber <= replaceRange.end.line && replaceRange.end.character < lineLength) ||
|
||||
(lineNumber > replaceRange.end.line && replaceRange.start.character < lineLength))
|
||||
) {
|
||||
rightPadding = longestLength - lineText.length;
|
||||
}
|
||||
transformations.push({
|
||||
type: 'replaceText',
|
||||
range,
|
||||
text: ' '.repeat(leftPadding) + lineText + ' '.repeat(rightPadding),
|
||||
});
|
||||
}
|
||||
}
|
||||
return transformations;
|
||||
}
|
||||
|
||||
if (mode === Mode.Normal || mode === Mode.Visual || mode === Mode.VisualLine) {
|
||||
return [
|
||||
{
|
||||
type: 'replaceText',
|
||||
range: replaceRange,
|
||||
text,
|
||||
},
|
||||
];
|
||||
} else if (mode === Mode.VisualBlock) {
|
||||
const transformations: Transformation[] = [];
|
||||
if (registerMode === RegisterMode.CharacterWise) {
|
||||
for (let line = replaceRange.start.line; line <= replaceRange.end.line; line++) {
|
||||
const range = new vscode.Range(
|
||||
new Position(line, replaceRange.start.character),
|
||||
new Position(line, replaceRange.end.character),
|
||||
);
|
||||
const lineText = !text.includes('\n') || line === replaceRange.start.line ? text : '';
|
||||
transformations.push({
|
||||
type: 'replaceText',
|
||||
range,
|
||||
text: lineText,
|
||||
});
|
||||
}
|
||||
} else if (registerMode === RegisterMode.LineWise) {
|
||||
// Weird case: first delete the block...
|
||||
for (let line = replaceRange.start.line; line <= replaceRange.end.line; line++) {
|
||||
const range = new vscode.Range(
|
||||
new Position(line, replaceRange.start.character),
|
||||
new Position(line, replaceRange.end.character),
|
||||
);
|
||||
transformations.push({
|
||||
type: 'replaceText',
|
||||
range,
|
||||
text: '',
|
||||
});
|
||||
}
|
||||
|
||||
// ...then paste the lines before/after the block
|
||||
const insertPos = this.putBefore()
|
||||
? new Position(replaceRange.start.line, 0)
|
||||
: new Position(replaceRange.end.line, 0).getLineEnd();
|
||||
transformations.push({
|
||||
type: 'replaceText',
|
||||
range: new vscode.Range(insertPos, insertPos),
|
||||
text,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unexpected RegisterMode ${registerMode}`);
|
||||
}
|
||||
return transformations;
|
||||
} else {
|
||||
throw new Error(`Unexpected Mode ${mode}`);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract putBefore(): boolean;
|
||||
|
||||
protected abstract getRegisterMode(register: IRegisterContent): RegisterMode;
|
||||
|
||||
protected abstract getReplaceRange(
|
||||
mode: Mode,
|
||||
cursor: Cursor,
|
||||
registerMode: RegisterMode,
|
||||
): vscode.Range;
|
||||
|
||||
protected abstract adjustLinewiseRegisterText(mode: Mode, text: string): string;
|
||||
|
||||
protected abstract shouldAdjustIndent(mode: Mode, registerMode: RegisterMode): boolean;
|
||||
|
||||
protected abstract getCursorPosition(params: GetCursorPositionParams): Position;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class PutCommand extends BasePutCommand {
|
||||
keys: string[] | string[][] = ['p'];
|
||||
|
||||
protected putBefore(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected getRegisterMode(register: IRegisterContent): RegisterMode {
|
||||
return register.registerMode;
|
||||
}
|
||||
|
||||
protected getReplaceRange(mode: Mode, cursor: Cursor, registerMode: RegisterMode): vscode.Range {
|
||||
if (mode === Mode.Normal) {
|
||||
let pos: Position;
|
||||
if (registerMode === RegisterMode.CharacterWise || registerMode === RegisterMode.BlockWise) {
|
||||
pos = cursor.stop.getRight();
|
||||
} else if (registerMode === RegisterMode.LineWise) {
|
||||
pos = cursor.stop.getLineEnd();
|
||||
} else {
|
||||
throw new Error(`Unexpected RegisterMode ${registerMode}`);
|
||||
}
|
||||
return new vscode.Range(pos, pos);
|
||||
} else if (mode === Mode.Visual) {
|
||||
const [start, end] = sorted(cursor.start, cursor.stop);
|
||||
return new vscode.Range(start, end.getRight());
|
||||
} else if (mode === Mode.VisualLine) {
|
||||
const [start, end] = sorted(cursor.start, cursor.stop);
|
||||
return new vscode.Range(start.getLineBegin(), end.getLineEnd());
|
||||
} else {
|
||||
const [start, end] = sorted(cursor.start, cursor.stop);
|
||||
return new vscode.Range(start, end.getRight());
|
||||
}
|
||||
}
|
||||
|
||||
protected adjustLinewiseRegisterText(mode: Mode, text: string): string {
|
||||
if (mode === Mode.Normal || mode === Mode.VisualBlock) {
|
||||
return '\n' + text;
|
||||
} else if (mode === Mode.Visual) {
|
||||
return '\n' + text + '\n';
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
protected shouldAdjustIndent(mode: Mode, registerMode: RegisterMode): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected getCursorPosition({
|
||||
mode,
|
||||
replaceRange,
|
||||
registerMode,
|
||||
text,
|
||||
returnToInsertAfterCommand,
|
||||
}: GetCursorPositionParams): Position {
|
||||
const rangeStart = replaceRange.start;
|
||||
if (mode === Mode.Normal || mode === Mode.Visual) {
|
||||
if (registerMode === RegisterMode.CharacterWise) {
|
||||
if (text.includes('\n')) {
|
||||
return rangeStart;
|
||||
} else if (returnToInsertAfterCommand) {
|
||||
return rangeStart.advancePositionByText(text);
|
||||
} else {
|
||||
return rangeStart.advancePositionByText(text).getLeft();
|
||||
}
|
||||
} else if (registerMode === RegisterMode.LineWise) {
|
||||
return new Position(rangeStart.line + 1, firstNonBlankChar(text));
|
||||
} else if (registerMode === RegisterMode.BlockWise) {
|
||||
return rangeStart;
|
||||
} else {
|
||||
throw new Error(`Unexpected RegisterMode ${registerMode}`);
|
||||
}
|
||||
} else if (mode === Mode.VisualLine) {
|
||||
return rangeStart.with({ character: firstNonBlankChar(text) });
|
||||
} else if (mode === Mode.VisualBlock) {
|
||||
if (registerMode === RegisterMode.LineWise) {
|
||||
return new Position(replaceRange.end.line + 1, firstNonBlankChar(text));
|
||||
} else if (registerMode === RegisterMode.BlockWise) {
|
||||
return rangeStart;
|
||||
} else {
|
||||
return rangeStart.with({ character: rangeStart.character + text.length - 1 });
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unexpected Mode ${mode}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class PutBeforeCommand extends PutCommand {
|
||||
override keys: string[] | string[][] = ['P'];
|
||||
|
||||
// Since Vim 9.0, Visual `P` does not overwrite the unnamed register with selection's contents
|
||||
override overwritesRegisterWithSelection = false;
|
||||
|
||||
protected override putBefore(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override adjustLinewiseRegisterText(mode: Mode, text: string): string {
|
||||
if (mode === Mode.Normal || mode === Mode.VisualBlock) {
|
||||
return text + '\n';
|
||||
}
|
||||
|
||||
return super.adjustLinewiseRegisterText(mode, text);
|
||||
}
|
||||
|
||||
protected override getReplaceRange(
|
||||
mode: Mode,
|
||||
cursor: Cursor,
|
||||
registerMode: RegisterMode,
|
||||
): vscode.Range {
|
||||
if (mode === Mode.Normal) {
|
||||
if (registerMode === RegisterMode.CharacterWise || registerMode === RegisterMode.BlockWise) {
|
||||
const pos = cursor.stop;
|
||||
return new vscode.Range(pos, pos);
|
||||
} else if (registerMode === RegisterMode.LineWise) {
|
||||
const pos = cursor.stop.getLineBegin();
|
||||
return new vscode.Range(pos, pos);
|
||||
}
|
||||
}
|
||||
|
||||
return super.getReplaceRange(mode, cursor, registerMode);
|
||||
}
|
||||
|
||||
protected override getCursorPosition({
|
||||
mode,
|
||||
replaceRange,
|
||||
text,
|
||||
registerMode,
|
||||
...params
|
||||
}: GetCursorPositionParams): Position {
|
||||
const rangeStart = replaceRange.start;
|
||||
if (mode === Mode.Normal || mode === Mode.VisualBlock) {
|
||||
if (registerMode === RegisterMode.LineWise) {
|
||||
return rangeStart.with({ character: firstNonBlankChar(text) });
|
||||
}
|
||||
}
|
||||
|
||||
return super.getCursorPosition({ mode, replaceRange, text, registerMode, ...params });
|
||||
}
|
||||
}
|
||||
|
||||
function PlaceCursorAfterText<TBase extends new (...args: any[]) => PutCommand>(Base: TBase) {
|
||||
return class CursorAfterText extends Base {
|
||||
protected override getCursorPosition({
|
||||
document,
|
||||
mode,
|
||||
replaceRange,
|
||||
registerMode,
|
||||
count,
|
||||
text,
|
||||
...params
|
||||
}: GetCursorPositionParams): Position {
|
||||
const rangeStart = replaceRange.start;
|
||||
if (mode === Mode.Normal || mode === Mode.Visual) {
|
||||
if (registerMode === RegisterMode.CharacterWise) {
|
||||
if (text.includes('\n')) {
|
||||
// Weird case: if there's a newline, the cursor goes to the same place, regardless of [count]
|
||||
// HACK: We're undoing the repeat() here - definitely a bit janky
|
||||
text = text.slice(0, text.length / count);
|
||||
}
|
||||
return rangeStart.advancePositionByText(text);
|
||||
} else if (registerMode === RegisterMode.LineWise) {
|
||||
let line = rangeStart.line + text.split('\n').length;
|
||||
if (
|
||||
mode === Mode.Visual ||
|
||||
(!this.putBefore() && rangeStart.line < document.lineCount - 1)
|
||||
) {
|
||||
line++;
|
||||
}
|
||||
return new Position(line, 0);
|
||||
} else if (registerMode === RegisterMode.BlockWise) {
|
||||
const lines = text.split('\n');
|
||||
const lastLine = rangeStart.line + lines.length - 1;
|
||||
const longestLineLength = Math.max(...lines.map((line) => line.length));
|
||||
return new Position(lastLine, rangeStart.character + longestLineLength);
|
||||
}
|
||||
} else if (mode === Mode.VisualLine) {
|
||||
return new Position(rangeStart.line + text.split('\n').length, 0);
|
||||
} else if (mode === Mode.VisualBlock) {
|
||||
const lines = text.split('\n');
|
||||
if (registerMode === RegisterMode.LineWise) {
|
||||
if (this.putBefore()) {
|
||||
return new Position(rangeStart.line + lines.length, 0);
|
||||
} else {
|
||||
return new Position(replaceRange.end.line + lines.length + 1, 0);
|
||||
}
|
||||
} else if (registerMode === RegisterMode.BlockWise) {
|
||||
return new Position(
|
||||
replaceRange.start.line + lines.length - 1,
|
||||
replaceRange.start.character + lines[lines.length - 1].length,
|
||||
);
|
||||
} else {
|
||||
return rangeStart.with({ character: rangeStart.character + text.length });
|
||||
}
|
||||
}
|
||||
|
||||
return super.getCursorPosition({
|
||||
document,
|
||||
mode,
|
||||
replaceRange,
|
||||
registerMode,
|
||||
count,
|
||||
text,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
@PlaceCursorAfterText
|
||||
class GPutCommand extends PutCommand {
|
||||
override keys = ['g', 'p'];
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
@PlaceCursorAfterText
|
||||
class GPutBeforeCommand extends PutBeforeCommand {
|
||||
override keys = ['g', 'P'];
|
||||
override overwritesRegisterWithSelection = true;
|
||||
}
|
||||
|
||||
function AdjustIndent<TBase extends new (...args: any[]) => PutCommand>(Base: TBase) {
|
||||
return class AdjustedIndent extends Base {
|
||||
protected override shouldAdjustIndent(mode: Mode, registerMode: RegisterMode): boolean {
|
||||
return (
|
||||
(mode === Mode.Normal || mode === Mode.VisualLine) && registerMode === RegisterMode.LineWise
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
@AdjustIndent
|
||||
class PutWithIndentCommand extends PutCommand {
|
||||
override keys = [']', 'p'];
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
@AdjustIndent
|
||||
class PutBeforeWithIndentCommand extends PutBeforeCommand {
|
||||
override keys = [
|
||||
['[', 'P'],
|
||||
[']', 'P'],
|
||||
['[', 'p'],
|
||||
];
|
||||
}
|
||||
|
||||
function ExCommand<TBase extends new (...args: any[]) => PutCommand>(Base: TBase) {
|
||||
return class Ex extends Base {
|
||||
private insertLine?: number;
|
||||
|
||||
public setInsertionLine(insertLine: number) {
|
||||
this.insertLine = insertLine;
|
||||
}
|
||||
|
||||
protected override getRegisterMode(register: IRegisterContent): RegisterMode {
|
||||
return RegisterMode.LineWise;
|
||||
}
|
||||
|
||||
protected override getReplaceRange(
|
||||
mode: Mode,
|
||||
cursor: Cursor,
|
||||
registerMode: RegisterMode,
|
||||
): vscode.Range {
|
||||
const line = this.insertLine ?? laterOf(cursor.start, cursor.stop).line;
|
||||
const pos = this.putBefore() ? new Position(line, 0) : new Position(line, 0).getLineEnd();
|
||||
return new vscode.Range(pos, pos);
|
||||
}
|
||||
|
||||
protected override getCursorPosition({
|
||||
replaceRange,
|
||||
text,
|
||||
}: GetCursorPositionParams): Position {
|
||||
const lines = text.split('\n');
|
||||
return new Position(
|
||||
replaceRange.start.line + lines.length - (this.putBefore() ? 1 : 0),
|
||||
firstNonBlankChar(lines[lines.length - 1]),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const PutFromCmdLine = ExCommand(PutCommand);
|
||||
export const PutBeforeFromCmdLine = ExCommand(PutBeforeCommand);
|
142
src/actions/commands/replace.ts
Normal file
142
src/actions/commands/replace.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { Position, Range } from 'vscode';
|
||||
import { PositionDiff } from '../../common/motion/position';
|
||||
import { Mode } from '../../mode/mode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { RegisterAction, BaseCommand } from '../base';
|
||||
|
||||
@RegisterAction
|
||||
class ExitReplaceMode extends BaseCommand {
|
||||
modes = [Mode.Replace];
|
||||
keys = [['<Esc>'], ['<C-c>'], ['<C-[>']];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
if (vimState.modeData.mode !== Mode.Replace) {
|
||||
throw new Error(`Unexpected mode ${vimState.modeData.mode} in ExitReplaceMode`);
|
||||
}
|
||||
|
||||
const timesToRepeat = vimState.modeData.replaceState.timesToRepeat;
|
||||
|
||||
const cursorIdx = this.multicursorIndex ?? 0;
|
||||
const changes = vimState.modeData.replaceState.getChanges(cursorIdx);
|
||||
|
||||
// `3Rabc` results in 'abc' replacing the next characters 2 more times
|
||||
if (changes && timesToRepeat > 1) {
|
||||
const newText = changes
|
||||
.map((change) => change.after)
|
||||
.join('')
|
||||
.repeat(timesToRepeat - 1);
|
||||
vimState.recordedState.transformer.replace(
|
||||
new Range(position, position.getRight(newText.length)),
|
||||
newText,
|
||||
);
|
||||
} else {
|
||||
vimState.cursorStopPosition = vimState.cursorStopPosition.getLeft();
|
||||
}
|
||||
|
||||
if (this.multicursorIndex === vimState.cursors.length - 1) {
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class ReplaceModeToInsertMode extends BaseCommand {
|
||||
modes = [Mode.Replace];
|
||||
keys = ['<Insert>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
await vimState.setCurrentMode(Mode.Insert);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class BackspaceInReplaceMode extends BaseCommand {
|
||||
modes = [Mode.Replace];
|
||||
keys = [['<BS>'], ['<S-BS>'], ['<C-BS>'], ['<C-h>']];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
if (vimState.modeData.mode !== Mode.Replace) {
|
||||
throw new Error(`Unexpected mode ${vimState.modeData.mode} in BackspaceInReplaceMode`);
|
||||
}
|
||||
|
||||
const cursorIdx = this.multicursorIndex ?? 0;
|
||||
const changes = vimState.modeData.replaceState.getChanges(cursorIdx);
|
||||
|
||||
if (changes.length === 0) {
|
||||
// If you backspace before the beginning of where you started to replace, just move the cursor back.
|
||||
const newPosition = position.getLeftThroughLineBreaks();
|
||||
|
||||
vimState.modeData.replaceState.resetChanges(cursorIdx);
|
||||
|
||||
vimState.cursorStopPosition = newPosition;
|
||||
vimState.cursorStartPosition = newPosition;
|
||||
} else {
|
||||
const { before } = changes.pop()!;
|
||||
if (before === '') {
|
||||
// We've gone beyond the originally existing text; just backspace.
|
||||
// TODO: should this use a 'deleteLeft' transformation?
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'deleteRange',
|
||||
range: new Range(position.getLeftThroughLineBreaks(), position),
|
||||
});
|
||||
} else {
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'replaceText',
|
||||
text: before,
|
||||
range: new Range(position.getLeft(), position),
|
||||
diff: PositionDiff.offset({ character: -1 }),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class ReplaceInReplaceMode extends BaseCommand {
|
||||
modes = [Mode.Replace];
|
||||
keys = ['<character>'];
|
||||
override createsUndoPoint = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
if (vimState.modeData.mode !== Mode.Replace) {
|
||||
throw new Error(`Unexpected mode ${vimState.modeData.mode} in ReplaceInReplaceMode`);
|
||||
}
|
||||
|
||||
const char = this.keysPressed[0];
|
||||
const isNewLineOrTab = char === '\n' || char === '<tab>';
|
||||
|
||||
const replaceRange = new Range(position, position.getRight());
|
||||
|
||||
let before = vimState.document.getText(replaceRange);
|
||||
if (!position.isLineEnd() && !isNewLineOrTab) {
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'replaceText',
|
||||
text: char,
|
||||
range: replaceRange,
|
||||
diff: PositionDiff.offset({ character: 1 }),
|
||||
});
|
||||
} else if (char === '<tab>') {
|
||||
vimState.recordedState.transformer.delete(replaceRange);
|
||||
vimState.recordedState.transformer.vscodeCommand('tab');
|
||||
} else {
|
||||
vimState.recordedState.transformer.insert(position, char);
|
||||
before = '';
|
||||
}
|
||||
|
||||
vimState.modeData.replaceState.getChanges(this.multicursorIndex ?? 0).push({
|
||||
before,
|
||||
after: char,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CreateUndoPoint extends BaseCommand {
|
||||
modes = [Mode.Replace];
|
||||
keys = ['<C-g>', 'u'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.historyTracker.addChange(true);
|
||||
vimState.historyTracker.finishCurrentStep();
|
||||
}
|
||||
}
|
342
src/actions/commands/scroll.ts
Normal file
342
src/actions/commands/scroll.ts
Normal file
@ -0,0 +1,342 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { clamp } from 'lodash';
|
||||
import { Position } from 'vscode';
|
||||
import { configuration } from '../../configuration/configuration';
|
||||
import { Mode, isVisualMode } from '../../mode/mode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { EditorScrollDirection, EditorScrollByUnit, TextEditor } from '../../textEditor';
|
||||
import { BaseCommand, RegisterAction } from '../base';
|
||||
|
||||
abstract class CommandEditorScroll extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
override runsOnceForEachCountPrefix = false;
|
||||
abstract to: EditorScrollDirection;
|
||||
abstract by: EditorScrollByUnit;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const timesToRepeat = vimState.recordedState.count || 1;
|
||||
const scrolloff = configuration
|
||||
.getConfiguration('editor')
|
||||
.get<number>('cursorSurroundingLines', 0);
|
||||
|
||||
const visibleRange = vimState.editor.visibleRanges[0];
|
||||
if (visibleRange === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linesAboveCursor =
|
||||
visibleRange.end.line - vimState.cursorStopPosition.line - timesToRepeat;
|
||||
const linesBelowCursor =
|
||||
vimState.cursorStopPosition.line - visibleRange.start.line - timesToRepeat;
|
||||
if (this.to === 'up' && scrolloff > linesAboveCursor) {
|
||||
vimState.cursorStopPosition = vimState.cursorStopPosition
|
||||
.getUp(scrolloff - linesAboveCursor)
|
||||
.withColumn(vimState.desiredColumn);
|
||||
} else if (this.to === 'down' && scrolloff > linesBelowCursor) {
|
||||
vimState.cursorStopPosition = vimState.cursorStopPosition
|
||||
.getDown(scrolloff - linesBelowCursor)
|
||||
.withColumn(vimState.desiredColumn);
|
||||
}
|
||||
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'editorScroll',
|
||||
args: {
|
||||
to: this.to,
|
||||
by: this.by,
|
||||
value: timesToRepeat,
|
||||
select: isVisualMode(vimState.currentMode),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCtrlE extends CommandEditorScroll {
|
||||
keys = ['<C-e>'];
|
||||
override preservesDesiredColumn = true;
|
||||
to: EditorScrollDirection = 'down';
|
||||
by: EditorScrollByUnit = 'line';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCtrlY extends CommandEditorScroll {
|
||||
keys = ['<C-y>'];
|
||||
override preservesDesiredColumn = true;
|
||||
to: EditorScrollDirection = 'up';
|
||||
by: EditorScrollByUnit = 'line';
|
||||
}
|
||||
|
||||
/**
|
||||
* Commands like `<C-d>` and `<C-f>` act *sort* of like `<count><C-e>`, but they move
|
||||
* your cursor down and put it on the first non-whitespace character of the line.
|
||||
*/
|
||||
abstract class CommandScrollAndMoveCursor extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
override runsOnceForEachCountPrefix = false;
|
||||
abstract to: EditorScrollDirection;
|
||||
/** if true, set scroll option instead of repeating command */
|
||||
setScroll = false;
|
||||
|
||||
/**
|
||||
* @returns the number of lines this command should move the cursor
|
||||
*/
|
||||
protected abstract getNumLines(visibleRanges: readonly vscode.Range[]): number;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const { visibleRanges } = vimState.editor;
|
||||
if (visibleRanges.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const smoothScrolling = configuration
|
||||
.getConfiguration('editor')
|
||||
.get<boolean>('smoothScrolling', false);
|
||||
|
||||
if (this.setScroll && vimState.recordedState.count)
|
||||
configuration.scroll = vimState.recordedState.count;
|
||||
const timesToRepeat = (!this.setScroll && vimState.recordedState.count) || 1;
|
||||
const moveLines = timesToRepeat * this.getNumLines(visibleRanges);
|
||||
|
||||
let scrollLines = moveLines;
|
||||
if (this.to === 'down') {
|
||||
// This makes <C-d> less wonky when `editor.scrollBeyondLastLine` is enabled
|
||||
scrollLines = Math.min(
|
||||
moveLines,
|
||||
vimState.document.lineCount - 1 - visibleRanges[visibleRanges.length - 1].end.line,
|
||||
);
|
||||
}
|
||||
|
||||
if (scrollLines > 0) {
|
||||
const args = {
|
||||
to: this.to,
|
||||
by: 'line',
|
||||
value: scrollLines,
|
||||
revealCursor: smoothScrolling,
|
||||
select: isVisualMode(vimState.currentMode),
|
||||
};
|
||||
if (smoothScrolling) {
|
||||
await vscode.commands.executeCommand('editorScroll', args);
|
||||
} else {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'editorScroll',
|
||||
args,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newPositionLine = clamp(
|
||||
position.line + (this.to === 'down' ? moveLines : -moveLines),
|
||||
0,
|
||||
vimState.document.lineCount - 1,
|
||||
);
|
||||
vimState.cursorStopPosition = new Position(
|
||||
newPositionLine,
|
||||
vimState.desiredColumn,
|
||||
).obeyStartOfLine(vimState.document);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandMoveFullPageUp extends CommandScrollAndMoveCursor {
|
||||
keys = ['<C-b>'];
|
||||
to: EditorScrollDirection = 'up';
|
||||
|
||||
protected getNumLines(visibleRanges: vscode.Range[]) {
|
||||
return visibleRanges[0].end.line - visibleRanges[0].start.line;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandMoveFullPageDown extends CommandScrollAndMoveCursor {
|
||||
keys = ['<C-f>'];
|
||||
to: EditorScrollDirection = 'down';
|
||||
|
||||
protected getNumLines(visibleRanges: vscode.Range[]) {
|
||||
return visibleRanges[0].end.line - visibleRanges[0].start.line;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCtrlD extends CommandScrollAndMoveCursor {
|
||||
keys = ['<C-d>'];
|
||||
to: EditorScrollDirection = 'down';
|
||||
override setScroll = true;
|
||||
|
||||
protected getNumLines(visibleRanges: vscode.Range[]) {
|
||||
return configuration.getScrollLines(visibleRanges);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCtrlU extends CommandScrollAndMoveCursor {
|
||||
keys = ['<C-u>'];
|
||||
to: EditorScrollDirection = 'up';
|
||||
override setScroll = true;
|
||||
|
||||
protected getNumLines(visibleRanges: vscode.Range[]) {
|
||||
return configuration.getScrollLines(visibleRanges);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCenterScroll extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
keys = ['z', 'z'];
|
||||
|
||||
override preservesDesiredColumn = true;
|
||||
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
// Don't run if there's an operator because the Sneak plugin uses <operator>z
|
||||
return (
|
||||
super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined
|
||||
);
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
// In these modes you want to center on the cursor position
|
||||
vimState.editor.revealRange(
|
||||
new vscode.Range(vimState.cursorStopPosition, vimState.cursorStopPosition),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandCenterScrollFirstChar extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
keys = ['z', '.'];
|
||||
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
// Don't run if there's an operator because the Sneak plugin uses <operator>z
|
||||
return (
|
||||
super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined
|
||||
);
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
// In these modes you want to center on the cursor position
|
||||
// This particular one moves cursor to first non blank char though
|
||||
vimState.editor.revealRange(
|
||||
new vscode.Range(vimState.cursorStopPosition, vimState.cursorStopPosition),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
);
|
||||
|
||||
// Move cursor to first char of line
|
||||
vimState.cursorStopPosition = TextEditor.getFirstNonWhitespaceCharOnLine(
|
||||
vimState.document,
|
||||
vimState.cursorStopPosition.line,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandTopScroll extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
keys = ['z', 't'];
|
||||
|
||||
override preservesDesiredColumn = true;
|
||||
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
// Don't run if there's an operator because the Sneak plugin uses <operator>z
|
||||
return (
|
||||
super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined
|
||||
);
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'revealLine',
|
||||
args: {
|
||||
lineNumber: position.line,
|
||||
at: 'top',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandTopScrollFirstChar extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
keys = ['z', '\n'];
|
||||
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
// Don't run if there's an operator because the Sneak plugin uses <operator>z
|
||||
return (
|
||||
super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined
|
||||
);
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
// In these modes you want to center on the cursor position
|
||||
// This particular one moves cursor to first non blank char though
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'revealLine',
|
||||
args: {
|
||||
lineNumber: position.line,
|
||||
at: 'top',
|
||||
},
|
||||
});
|
||||
|
||||
// Move cursor to first char of line
|
||||
vimState.cursorStopPosition = TextEditor.getFirstNonWhitespaceCharOnLine(
|
||||
vimState.document,
|
||||
vimState.cursorStopPosition.line,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandBottomScroll extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
keys = ['z', 'b'];
|
||||
|
||||
override preservesDesiredColumn = true;
|
||||
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
// Don't run if there's an operator because the Sneak plugin uses <operator>z
|
||||
return (
|
||||
super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined
|
||||
);
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'revealLine',
|
||||
args: {
|
||||
lineNumber: position.line,
|
||||
at: 'bottom',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandBottomScrollFirstChar extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
keys = ['z', '-'];
|
||||
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
// Don't run if there's an operator because the Sneak plugin uses <operator>z
|
||||
return (
|
||||
super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined
|
||||
);
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
// In these modes you want to center on the cursor position
|
||||
// This particular one moves cursor to first non blank char though
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'revealLine',
|
||||
args: {
|
||||
lineNumber: position.line,
|
||||
at: 'bottom',
|
||||
},
|
||||
});
|
||||
|
||||
// Move cursor to first char of line
|
||||
vimState.cursorStopPosition = TextEditor.getFirstNonWhitespaceCharOnLine(
|
||||
vimState.document,
|
||||
vimState.cursorStopPosition.line,
|
||||
);
|
||||
}
|
||||
}
|
313
src/actions/commands/search.ts
Normal file
313
src/actions/commands/search.ts
Normal file
@ -0,0 +1,313 @@
|
||||
import * as _ from 'lodash';
|
||||
import { escapeRegExp } from 'lodash';
|
||||
import { Position, Selection } from 'vscode';
|
||||
import { SearchCommandLine } from '../../cmd_line/commandLine';
|
||||
import { sorted } from '../../common/motion/position';
|
||||
import { configuration } from '../../configuration/configuration';
|
||||
import { ErrorCode, VimError } from '../../error';
|
||||
import { Mode, isVisualMode } from '../../mode/mode';
|
||||
import { Register } from '../../register/register';
|
||||
import { globalState } from '../../state/globalState';
|
||||
import { SearchState } from '../../state/searchState';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { TextEditor } from '../../textEditor';
|
||||
import { TextObject } from '../../textobject/textobject';
|
||||
import { reportSearch } from '../../util/statusBarTextUtils';
|
||||
import { SearchDirection } from '../../vimscript/pattern';
|
||||
import { BaseCommand, RegisterAction } from '../base';
|
||||
import { IMovement, failedMovement } from '../baseMotion';
|
||||
|
||||
/**
|
||||
* Search for the word under the cursor; used by [g]* and [g]#
|
||||
*/
|
||||
async function searchCurrentWord(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
direction: SearchDirection,
|
||||
isExact: boolean,
|
||||
): Promise<void> {
|
||||
let currentWord = TextEditor.getWord(vimState.document, position);
|
||||
|
||||
if (currentWord) {
|
||||
if (/\W/.test(currentWord[0]) || /\W/.test(currentWord[currentWord.length - 1])) {
|
||||
// TODO: this kind of sucks. JS regex does not consider the boundary between a special
|
||||
// character and whitespace to be a "word boundary", so we can't easily do an exact search.
|
||||
isExact = false;
|
||||
}
|
||||
|
||||
if (isExact) {
|
||||
currentWord = _.escapeRegExp(currentWord);
|
||||
}
|
||||
// If the search is going left then use `getWordLeft()` on position to start
|
||||
// at the beginning of the word. This ensures that any matches happen
|
||||
// outside of the currently selected word.
|
||||
const searchStartCursorPosition =
|
||||
direction === SearchDirection.Backward
|
||||
? vimState.cursorStopPosition.prevWordStart(vimState.document, { inclusive: true })
|
||||
: vimState.cursorStopPosition;
|
||||
|
||||
await createSearchStateAndMoveToMatch({
|
||||
needle: currentWord,
|
||||
vimState,
|
||||
direction,
|
||||
isExact,
|
||||
searchStartCursorPosition,
|
||||
});
|
||||
} else {
|
||||
StatusBar.displayError(vimState, VimError.fromCode(ErrorCode.NoStringUnderCursor));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for the word under the cursor; used by [g]* and [g]# in visual mode when `visualstar` is enabled
|
||||
*/
|
||||
async function searchCurrentSelection(vimState: VimState, direction: SearchDirection) {
|
||||
const currentSelection = vimState.document.getText(vimState.editor.selection);
|
||||
|
||||
// Go back to Normal mode, otherwise the selection grows to the next match.
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
|
||||
const [start, end] = sorted(vimState.cursorStartPosition, vimState.cursorStopPosition);
|
||||
|
||||
// Ensure that any matches happen outside of the currently selected word.
|
||||
const searchStartCursorPosition =
|
||||
direction === SearchDirection.Backward ? start.getLeft() : end.getRight();
|
||||
|
||||
await createSearchStateAndMoveToMatch({
|
||||
needle: currentSelection,
|
||||
vimState,
|
||||
direction,
|
||||
isExact: false,
|
||||
searchStartCursorPosition,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by [g]* and [g]#
|
||||
*/
|
||||
async function createSearchStateAndMoveToMatch(args: {
|
||||
needle: string;
|
||||
vimState: VimState;
|
||||
direction: SearchDirection;
|
||||
isExact: boolean;
|
||||
searchStartCursorPosition: Position;
|
||||
}): Promise<void> {
|
||||
const { needle, vimState, isExact } = args;
|
||||
|
||||
if (needle.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const escapedNeedle = escapeRegExp(needle).replace('/', '\\/');
|
||||
const searchString = isExact ? `\\<${escapedNeedle}\\>` : escapedNeedle;
|
||||
|
||||
// Start a search for the given term.
|
||||
globalState.searchState = new SearchState(
|
||||
args.direction,
|
||||
vimState.cursorStopPosition,
|
||||
searchString,
|
||||
{ ignoreSmartcase: true },
|
||||
);
|
||||
Register.setReadonlyRegister('/', globalState.searchState.searchString);
|
||||
void SearchCommandLine.addSearchStateToHistory(globalState.searchState);
|
||||
|
||||
// Turn one of the highlighting flags back on (turned off with :nohl)
|
||||
globalState.hl = true;
|
||||
|
||||
const nextMatch = globalState.searchState.getNextSearchMatchPosition(
|
||||
vimState,
|
||||
args.searchStartCursorPosition,
|
||||
);
|
||||
if (nextMatch) {
|
||||
vimState.cursorStopPosition = nextMatch.pos;
|
||||
|
||||
reportSearch(
|
||||
nextMatch.index,
|
||||
globalState.searchState.getMatchRanges(vimState).length,
|
||||
vimState,
|
||||
);
|
||||
} else {
|
||||
StatusBar.displayError(
|
||||
vimState,
|
||||
VimError.fromCode(
|
||||
args.direction === SearchDirection.Forward
|
||||
? ErrorCode.SearchHitBottom
|
||||
: ErrorCode.SearchHitTop,
|
||||
globalState.searchState.searchString,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSearchCurrentWordExactForward extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = ['*'];
|
||||
override actionType = 'motion' as const;
|
||||
override runsOnceForEachCountPrefix = true;
|
||||
override isJump = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
if (isVisualMode(vimState.currentMode) && configuration.visualstar) {
|
||||
await searchCurrentSelection(vimState, SearchDirection.Forward);
|
||||
} else {
|
||||
await searchCurrentWord(position, vimState, SearchDirection.Forward, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSearchCurrentWordForward extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = ['g', '*'];
|
||||
override actionType = 'motion' as const;
|
||||
override runsOnceForEachCountPrefix = true;
|
||||
override isJump = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
await searchCurrentWord(position, vimState, SearchDirection.Forward, false);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSearchCurrentWordExactBackward extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = ['#'];
|
||||
override actionType = 'motion' as const;
|
||||
override runsOnceForEachCountPrefix = true;
|
||||
override isJump = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
if (isVisualMode(vimState.currentMode) && configuration.visualstar) {
|
||||
await searchCurrentSelection(vimState, SearchDirection.Backward);
|
||||
} else {
|
||||
await searchCurrentWord(position, vimState, SearchDirection.Backward, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSearchCurrentWordBackward extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = ['g', '#'];
|
||||
override actionType = 'motion' as const;
|
||||
override runsOnceForEachCountPrefix = true;
|
||||
override isJump = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
await searchCurrentWord(position, vimState, SearchDirection.Backward, false);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSearchForwards extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
keys = ['/'];
|
||||
override actionType = 'motion' as const;
|
||||
override isJump = true;
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
await vimState.setCurrentMode(Mode.SearchInProgressMode);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSearchBackwards extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
keys = ['?'];
|
||||
override actionType = 'motion' as const;
|
||||
override isJump = true;
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
// TODO: Better VimState API than this...
|
||||
await vimState.setModeData({
|
||||
mode: Mode.SearchInProgressMode,
|
||||
commandLine: new SearchCommandLine(vimState, '', SearchDirection.Backward),
|
||||
firstVisibleLineBeforeSearch: vimState.editor.visibleRanges[0].start.line,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SearchObject extends TextObject {
|
||||
override modes = [Mode.Normal, Mode.Visual, Mode.VisualBlock];
|
||||
protected abstract readonly direction: SearchDirection;
|
||||
|
||||
public async execAction(position: Position, vimState: VimState): Promise<IMovement> {
|
||||
const searchState = globalState.searchState;
|
||||
if (!searchState || searchState.searchString === '') {
|
||||
return failedMovement(vimState);
|
||||
}
|
||||
|
||||
const newSearchState = new SearchState(
|
||||
this.direction,
|
||||
vimState.cursorStopPosition,
|
||||
searchState.searchString,
|
||||
{},
|
||||
);
|
||||
|
||||
// At first, try to search for current word, and stop searching if matched.
|
||||
// Try to search for the next word if not matched or
|
||||
// if the cursor is at the end of a match string in visual-mode.
|
||||
let result = newSearchState.findContainingMatchRange(vimState, vimState.cursorStopPosition);
|
||||
if (
|
||||
result &&
|
||||
vimState.currentMode === Mode.Visual &&
|
||||
vimState.cursorStopPosition.isEqual(result.range.end.getLeftThroughLineBreaks())
|
||||
) {
|
||||
result = undefined;
|
||||
}
|
||||
|
||||
if (result === undefined) {
|
||||
// Try to search for the next word
|
||||
result = newSearchState.getNextSearchMatchRange(vimState, vimState.cursorStopPosition);
|
||||
if (result === undefined) {
|
||||
return failedMovement(vimState);
|
||||
}
|
||||
}
|
||||
|
||||
reportSearch(result.index, searchState.getMatchRanges(vimState).length, vimState);
|
||||
|
||||
const [start, stop] = [
|
||||
vimState.currentMode === Mode.Normal ? result.range.start : vimState.cursorStopPosition,
|
||||
result.range.end.getLeftThroughLineBreaks(),
|
||||
];
|
||||
|
||||
// Move the cursor, this is a bit hacky...
|
||||
vimState.cursorStartPosition = start;
|
||||
vimState.cursorStopPosition = stop;
|
||||
vimState.editor.selection = new Selection(start, stop);
|
||||
|
||||
await vimState.setCurrentMode(Mode.Visual);
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
}
|
||||
|
||||
public override async execActionForOperator(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
): Promise<IMovement> {
|
||||
return this.execAction(position, vimState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class SearchObjectForward extends SearchObject {
|
||||
keys = ['g', 'n'];
|
||||
direction = SearchDirection.Forward;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class SearchObjectBackward extends SearchObject {
|
||||
keys = ['g', 'N'];
|
||||
direction = SearchDirection.Backward;
|
||||
}
|
220
src/actions/commands/window.ts
Normal file
220
src/actions/commands/window.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import { Position } from 'vscode';
|
||||
import { OnlyCommand } from '../../cmd_line/commands/only';
|
||||
import { QuitCommand } from '../../cmd_line/commands/quit';
|
||||
import { Mode } from '../../mode/mode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { BaseCommand, RegisterAction } from '../base';
|
||||
|
||||
@RegisterAction
|
||||
class CommandQuit extends BaseCommand {
|
||||
modes = [Mode.Normal];
|
||||
keys = [
|
||||
['<C-w>', 'q'],
|
||||
['<C-w>', '<C-q>'],
|
||||
['<C-w>', 'c'],
|
||||
['<C-w>', '<C-c>'],
|
||||
];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
void new QuitCommand({}).execute(vimState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandOnly extends BaseCommand {
|
||||
modes = [Mode.Normal];
|
||||
keys = [
|
||||
['<C-w>', 'o'],
|
||||
['<C-w>', '<C-o>'],
|
||||
];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
void new OnlyCommand().execute(vimState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveToRightPane extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = [
|
||||
['<C-w>', 'l'],
|
||||
['<C-w>', '<right>'],
|
||||
['<C-w>', '<C-l>'],
|
||||
];
|
||||
override isJump = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.navigateRight',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveToLowerPane extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = [
|
||||
['<C-w>', 'j'],
|
||||
['<C-w>', '<down>'],
|
||||
['<C-w>', '<C-j>'],
|
||||
];
|
||||
override isJump = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.navigateDown',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveToUpperPane extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = [
|
||||
['<C-w>', 'k'],
|
||||
['<C-w>', '<up>'],
|
||||
['<C-w>', '<C-k>'],
|
||||
];
|
||||
override isJump = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.navigateUp',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveToLeftPane extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = [
|
||||
['<C-w>', 'h'],
|
||||
['<C-w>', '<left>'],
|
||||
['<C-w>', '<C-h>'],
|
||||
];
|
||||
override isJump = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.navigateLeft',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CycleThroughPanes extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = [
|
||||
['<C-w>', '<C-w>'],
|
||||
['<C-w>', 'w'],
|
||||
];
|
||||
override isJump = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.navigateEditorGroups',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class VerticalSplit extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = [
|
||||
['<C-w>', 'v'],
|
||||
['<C-w>', '<C-v>'],
|
||||
];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.splitEditor',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class OrthogonalSplit extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = [
|
||||
['<C-w>', 's'],
|
||||
['<C-w>', '<C-s>'],
|
||||
];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.splitEditorOrthogonal',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EvenPaneWidths extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = ['<C-w>', '='];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.evenEditorWidths',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class IncreasePaneWidth extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = ['<C-w>', '>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.increaseViewWidth',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class DecreasePaneWidth extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = ['<C-w>', '<'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.decreaseViewWidth',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class IncreasePaneHeight extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = ['<C-w>', '+'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.increaseViewHeight',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class DecreasePaneHeight extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
keys = ['<C-w>', '-'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
vimState.postponedCodeViewChanges.push({
|
||||
command: 'workbench.action.decreaseViewHeight',
|
||||
args: {},
|
||||
});
|
||||
}
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
import { Selection, commands, window } from 'vscode';
|
||||
import { Action } from '../action_types';
|
||||
import { Mode } from '../modes_types';
|
||||
import { parseKeysExact } from '../parse_keys';
|
||||
|
||||
// https://docs.helix-editor.com/keymap.html#goto-mode
|
||||
export const gotoActions: Action[] = [
|
||||
// G actions
|
||||
parseKeysExact(['g', '.'], [Mode.Normal], () => {
|
||||
commands.executeCommand('workbench.action.navigateToLastEditLocation');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'e'], [Mode.Normal], () => {
|
||||
commands.executeCommand('cursorBottom');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'e'], [Mode.Visual], () => {
|
||||
commands.executeCommand('cursorBottomSelect');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'g'], [Mode.Normal], (helixState, editor) => {
|
||||
const count = helixState.resolveCount();
|
||||
if (count !== 1) {
|
||||
const range = editor.document.lineAt(count - 1).range;
|
||||
editor.selection = new Selection(range.start, range.end);
|
||||
editor.revealRange(range);
|
||||
return;
|
||||
}
|
||||
|
||||
commands.executeCommand('cursorTop');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'g'], [Mode.Visual], (helixState, editor) => {
|
||||
const count = helixState.resolveCount();
|
||||
if (count !== 1) {
|
||||
const position = editor.selection.active;
|
||||
const range = editor.document.lineAt(count - 1).range;
|
||||
if (position.isBefore(range.start)) {
|
||||
editor.selection = new Selection(position, range.end);
|
||||
} else {
|
||||
editor.selection = new Selection(position, range.start);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
commands.executeCommand('cursorTopSelect');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'h'], [Mode.Normal], () => {
|
||||
commands.executeCommand('cursorLineStart');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'h'], [Mode.Visual], () => {
|
||||
commands.executeCommand('cursorLineStartSelect');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'l'], [Mode.Normal], () => {
|
||||
commands.executeCommand('cursorLineEnd');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'l'], [Mode.Visual], () => {
|
||||
commands.executeCommand('cursorLineEndSelect');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 's'], [Mode.Normal], () => {
|
||||
commands.executeCommand('cursorHome');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 's'], [Mode.Visual], () => {
|
||||
commands.executeCommand('cursorHomeSelect');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'd'], [Mode.Normal], () => {
|
||||
commands.executeCommand('editor.action.revealDefinition');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'y'], [Mode.Normal], () => {
|
||||
commands.executeCommand('editor.action.goToTypeDefinition');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'r'], [Mode.Normal], () => {
|
||||
commands.executeCommand('editor.action.goToReferences');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 't'], [Mode.Normal], () => {
|
||||
commands.executeCommand('cursorPageUp');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 't'], [Mode.Visual], () => {
|
||||
commands.executeCommand('cursorPageUpSelect');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'b'], [Mode.Normal], () => {
|
||||
commands.executeCommand('cursorPageDown');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'b'], [Mode.Visual], () => {
|
||||
commands.executeCommand('cursorPageDownSelect');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'c'], [Mode.Normal], () => {
|
||||
commands.executeCommand('cursorMove', {
|
||||
to: 'viewPortCenter',
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'c'], [Mode.Visual], () => {
|
||||
commands.executeCommand('cursorMove', {
|
||||
to: 'viewPortCenter',
|
||||
select: true,
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'k'], [Mode.Normal], () => {
|
||||
commands.executeCommand('scrollLineUp');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'j'], [Mode.Normal], () => {
|
||||
commands.executeCommand('scrollLineDown');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'a'], [Mode.Normal], (helixState) => {
|
||||
// VS Code has no concept of "last accessed file" so instead we'll need to keep track of previous text editors
|
||||
const editor = helixState.editorState.previousEditor;
|
||||
if (!editor) return;
|
||||
|
||||
window.showTextDocument(editor.document);
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'm'], [Mode.Normal], (helixState) => {
|
||||
// VS Code has no concept of "last accessed file" so instead we'll need to keep track of previous text editors
|
||||
const document = helixState.editorState.lastModifiedDocument;
|
||||
if (!document) return;
|
||||
|
||||
window.showTextDocument(document);
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'n'], [Mode.Normal], () => {
|
||||
commands.executeCommand('workbench.action.nextEditor');
|
||||
}),
|
||||
|
||||
parseKeysExact(['g', 'p'], [Mode.Normal], () => {
|
||||
commands.executeCommand('workbench.action.previousEditor');
|
||||
}),
|
||||
];
|
16
src/actions/include-main.ts
Normal file
16
src/actions/include-main.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import './base';
|
||||
import './operator';
|
||||
import './motion';
|
||||
import '../textobject/textobject';
|
||||
|
||||
// commands
|
||||
import './commands/insert';
|
||||
import './commands/replace';
|
||||
import './commands/actions';
|
||||
import './commands/commandLine';
|
||||
import './commands/search';
|
||||
import './commands/put';
|
||||
import './commands/digraphs';
|
||||
import './commands/window';
|
||||
import './commands/fold';
|
||||
import './commands/scroll';
|
8
src/actions/include-plugins.ts
Normal file
8
src/actions/include-plugins.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// plugin
|
||||
import './plugins/camelCaseMotion';
|
||||
import './plugins/easymotion/easymotion.cmd';
|
||||
import './plugins/easymotion/registerMoveActions';
|
||||
import './plugins/sneak';
|
||||
import './plugins/replaceWithRegister';
|
||||
import './plugins/surround';
|
||||
import './plugins/targets/targets';
|
@ -1,6 +0,0 @@
|
||||
import { Action } from '../action_types'
|
||||
import { actions as subActions } from './actions'
|
||||
import { operators } from './operators'
|
||||
import { motions } from './motions'
|
||||
|
||||
export const actions: Action[] = subActions.concat(operators, motions)
|
@ -1,17 +0,0 @@
|
||||
export default {
|
||||
Motions: {
|
||||
MoveLeft: 'h',
|
||||
MoveRight: 'l',
|
||||
MoveDown: 'j',
|
||||
MoveUp: 'k',
|
||||
MoveLineEnd: 'o',
|
||||
MoveLineStart: 'u',
|
||||
},
|
||||
Actions: {
|
||||
InsertMode: 'i',
|
||||
InsertAtLineStart: 'I',
|
||||
InsertAtLineEnd: 'A',
|
||||
NewLineAbove: 'O',
|
||||
NewLineBelow: 'o',
|
||||
},
|
||||
};
|
249
src/actions/languages/python/motion.ts
Normal file
249
src/actions/languages/python/motion.ts
Normal file
@ -0,0 +1,249 @@
|
||||
import { Position, TextDocument } from 'vscode';
|
||||
import { VimState } from '../../../state/vimState';
|
||||
import { RegisterAction } from '../../base';
|
||||
import { BaseMovement, failedMovement, IMovement } from '../../baseMotion';
|
||||
|
||||
type Type = 'function' | 'class';
|
||||
type Edge = 'start' | 'end';
|
||||
type Direction = 'next' | 'prev';
|
||||
|
||||
interface LineInfo {
|
||||
line: number;
|
||||
indentation: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface StructureElement {
|
||||
type: Type;
|
||||
start: Position;
|
||||
end: Position;
|
||||
}
|
||||
|
||||
// Older browsers don't support lookbehind - in this case, use an inferior regex rather than crashing
|
||||
let supportsLookbehind = true;
|
||||
try {
|
||||
new RegExp('(?<=x)');
|
||||
} catch {
|
||||
supportsLookbehind = false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Utility class used to parse the lines in the document and
|
||||
* determine class and function boundaries
|
||||
*
|
||||
* The class keeps track of two positions: the ORIGINAL and the CURRENT
|
||||
* using their relative locations to make decisions.
|
||||
*/
|
||||
export class PythonDocument {
|
||||
_document: TextDocument;
|
||||
structure: StructureElement[];
|
||||
|
||||
static readonly reOnlyWhitespace = /\S/;
|
||||
static readonly reLastNonWhiteSpaceCharacter = supportsLookbehind
|
||||
? new RegExp('(?<=\\S)\\s*$')
|
||||
: /(\S)\s*$/;
|
||||
static readonly reDefOrClass = /^\s*(def|class) /;
|
||||
|
||||
constructor(document: TextDocument) {
|
||||
this._document = document;
|
||||
const parsed = PythonDocument._parseLines(document);
|
||||
this.structure = PythonDocument._parseStructure(parsed);
|
||||
}
|
||||
|
||||
/*
|
||||
* Generator of the lines of text in the document
|
||||
*/
|
||||
static *lines(document: TextDocument): Generator<string> {
|
||||
for (let index = 0; index < document.lineCount; index++) {
|
||||
yield document.lineAt(index).text;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the indentation of a line of text.
|
||||
* Lines consisting entirely of whitespace of "starting" with a comment are defined
|
||||
* to have an indentation of "undefined".
|
||||
*/
|
||||
static _indentation(line: string): number | undefined {
|
||||
const index: number = line.search(PythonDocument.reOnlyWhitespace);
|
||||
|
||||
// Return undefined if line is empty, just whitespace, or starts with a comment
|
||||
if (index === -1 || line[index] === '#') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse a line of text to extract LineInfo
|
||||
* Return undefined if the line is empty or starts with a comment
|
||||
*/
|
||||
static _parseLine(index: number, text: string): LineInfo | undefined {
|
||||
const indentation = this._indentation(text);
|
||||
|
||||
// Since indentation === 0 is a valid result we need to check for undefined explicitly
|
||||
return indentation !== undefined ? { line: index, indentation, text } : undefined;
|
||||
}
|
||||
|
||||
static _parseLines(document: TextDocument): LineInfo[] {
|
||||
const lines = [...this.lines(document)]; // convert generator to Array
|
||||
const infos = lines.map((text, index) => this._parseLine(index, text));
|
||||
|
||||
return infos.filter((x) => x) as LineInfo[]; // filter out empty/comment lines (undefined info)
|
||||
}
|
||||
|
||||
static _parseStructure(lines: LineInfo[]): StructureElement[] {
|
||||
const last = lines.length;
|
||||
const structure: StructureElement[] = [];
|
||||
|
||||
for (let index = 0; index < last; index++) {
|
||||
const info = lines[index];
|
||||
const text = info.text;
|
||||
const match = text.match(PythonDocument.reDefOrClass);
|
||||
|
||||
if (match) {
|
||||
const type = match[1] === 'def' ? 'function' : 'class';
|
||||
|
||||
// Find the end of the current function/class
|
||||
let idx = index + 1;
|
||||
|
||||
for (; idx < last; idx++) {
|
||||
if (lines[idx].indentation <= info.indentation) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Since we stop when we find the first line with a less indentation
|
||||
// we pull back one line to get to the end of the function/class
|
||||
idx--;
|
||||
|
||||
const endLine = lines[idx];
|
||||
|
||||
structure.push({
|
||||
type,
|
||||
start: new Position(info.line, info.indentation),
|
||||
// Calculate position of last non-white character)
|
||||
end: new Position(
|
||||
endLine.line,
|
||||
endLine.text.search(PythonDocument.reLastNonWhiteSpaceCharacter) - 1,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return structure;
|
||||
}
|
||||
|
||||
/*
|
||||
* Find the position of the specified:
|
||||
* type: function or class
|
||||
* direction: next or prev
|
||||
* edge: start or end
|
||||
*
|
||||
* With this information one can determine all of the required motions
|
||||
*/
|
||||
find(type: Type, direction: Direction, edge: Edge, position: Position): Position | undefined {
|
||||
// Choose the ordering method name based on direction
|
||||
const isDirection = direction === 'next' ? 'isAfter' : 'isBefore';
|
||||
|
||||
// Filter function for all elements whose "edge" is in the correct "direction"
|
||||
// relative to the cursor's position
|
||||
const dir = (element: StructureElement) => element[edge][isDirection](position);
|
||||
|
||||
// Filter out elements from structure based on type and direction
|
||||
const elements = this.structure.filter((elem) => elem.type === type).filter(dir);
|
||||
|
||||
if (edge === 'end') {
|
||||
// When moving to an 'end' the elements should be started by the end position
|
||||
elements.sort((a, b) => a.end.line - b.end.line);
|
||||
}
|
||||
|
||||
// Return the first match if any exist
|
||||
if (elements.length) {
|
||||
// If direction === 'next' return the first element
|
||||
// otherwise return the last element
|
||||
const index = direction === 'next' ? 0 : elements.length - 1;
|
||||
const element = elements[index];
|
||||
const pos = element[edge];
|
||||
|
||||
// execAction MUST return a fully realized Position object created using new
|
||||
return pos;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Use PythonDocument instance to move to specified class boundary
|
||||
static moveClassBoundary(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
forward: boolean,
|
||||
start: boolean,
|
||||
): Position | IMovement {
|
||||
const direction = forward ? 'next' : 'prev';
|
||||
const edge = start ? 'start' : 'end';
|
||||
|
||||
return (
|
||||
new PythonDocument(document).find('class', direction, edge, position) ??
|
||||
failedMovement(vimState)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Uses the specified findFunction to execute the motion coupled to the shortcut (keys)
|
||||
abstract class BasePythonMovement extends BaseMovement {
|
||||
abstract type: Type;
|
||||
abstract direction: Direction;
|
||||
abstract edge: Edge;
|
||||
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
return (
|
||||
super.doesActionApply(vimState, keysPressed) && vimState.document.languageId === 'python'
|
||||
);
|
||||
}
|
||||
|
||||
public override async execAction(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
): Promise<Position | IMovement> {
|
||||
const document = vimState.document;
|
||||
return (
|
||||
new PythonDocument(document).find(this.type, this.direction, this.edge, position) ??
|
||||
failedMovement(vimState)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MovePythonNextFunctionStart extends BasePythonMovement {
|
||||
keys = [']', 'm'];
|
||||
type: Type = 'function';
|
||||
direction: Direction = 'next';
|
||||
edge: Edge = 'start';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MovePythonPrevFunctionStart extends BasePythonMovement {
|
||||
keys = ['[', 'm'];
|
||||
type: Type = 'function';
|
||||
direction: Direction = 'prev';
|
||||
edge: Edge = 'start';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MovePythonNextFunctionEnd extends BasePythonMovement {
|
||||
keys = [']', 'M'];
|
||||
type: Type = 'function';
|
||||
direction: Direction = 'next';
|
||||
edge: Edge = 'end';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MovePythonPrevFunctionEnd extends BasePythonMovement {
|
||||
keys = ['[', 'M'];
|
||||
type: Type = 'function';
|
||||
direction: Direction = 'prev';
|
||||
edge: Edge = 'end';
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Action } from '../action_types';
|
||||
import { enterInsertMode, setModeCursorStyle } from '../modes';
|
||||
import { Mode } from '../modes_types';
|
||||
import { parseKeysExact, parseKeysRegex } from '../parse_keys';
|
||||
import { searchBackwardBracket, searchForwardBracket } from '../search_utils';
|
||||
import { removeTypeSubscription } from '../type_subscription';
|
||||
import { delete_, yank } from './operators';
|
||||
|
||||
export const matchActions: Action[] = [
|
||||
// Implemenent jump to bracket
|
||||
parseKeysExact(['m', 'm'], [Mode.Normal], () => {
|
||||
vscode.commands.executeCommand('editor.action.jumpToBracket');
|
||||
}),
|
||||
|
||||
parseKeysExact(['m', 'm'], [Mode.Visual], () => {
|
||||
vscode.commands.executeCommand('editor.action.selectToBracket');
|
||||
}),
|
||||
|
||||
// Delete match
|
||||
parseKeysExact(['d'], [Mode.Normal, Mode.Visual], (helixState, editor) => {
|
||||
const ranges = editor.selections.map((selection) => selection.with());
|
||||
yank(helixState, editor, ranges, false);
|
||||
delete_(editor, ranges, false);
|
||||
}),
|
||||
|
||||
// edit match
|
||||
parseKeysExact(['c'], [Mode.Normal, Mode.Visual], (helixState, editor) => {
|
||||
const ranges = editor.selections.map((selection) => selection.with());
|
||||
delete_(editor, ranges, false);
|
||||
enterInsertMode(helixState);
|
||||
setModeCursorStyle(helixState.mode, editor);
|
||||
removeTypeSubscription(helixState);
|
||||
}),
|
||||
|
||||
// implement match add to selection
|
||||
parseKeysRegex(/^ms(.)$/, /^ms/, [Mode.Normal, Mode.Visual], (helixState, editor, match) => {
|
||||
const char = match[1];
|
||||
const [startChar, endChar] = getMatchPairs(char);
|
||||
// Add char to both ends of each selection
|
||||
editor.edit((editBuilder) => {
|
||||
// Add char to both ends of each selection
|
||||
editor.selections.forEach((selection) => {
|
||||
const start = selection.start;
|
||||
const end = selection.end;
|
||||
editBuilder.insert(start, startChar);
|
||||
editBuilder.insert(end, endChar);
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
// implement match replace to selection
|
||||
parseKeysRegex(/^mr(.)(.)$/, /^mr(.)?/, [Mode.Normal, Mode.Visual], (helixState, editor, match) => {
|
||||
const original = match[1];
|
||||
const replacement = match[2];
|
||||
const [startCharOrig, endCharOrig] = getMatchPairs(original);
|
||||
const [startCharNew, endCharNew] = getMatchPairs(replacement);
|
||||
const num = helixState.resolveCount();
|
||||
|
||||
const forwardPosition = searchForwardBracket(
|
||||
editor.document,
|
||||
startCharOrig,
|
||||
endCharOrig,
|
||||
editor.selection.active,
|
||||
num,
|
||||
);
|
||||
const backwardPosition = searchBackwardBracket(
|
||||
editor.document,
|
||||
startCharOrig,
|
||||
endCharOrig,
|
||||
editor.selection.active,
|
||||
num,
|
||||
);
|
||||
|
||||
if (forwardPosition === undefined || backwardPosition === undefined) return;
|
||||
|
||||
// Add char to both ends of each selection
|
||||
editor.edit((editBuilder) => {
|
||||
// Add char to both ends of each selection
|
||||
editBuilder.replace(
|
||||
new vscode.Range(forwardPosition, forwardPosition.with({ character: forwardPosition.character + 1 })),
|
||||
endCharNew,
|
||||
);
|
||||
editBuilder.replace(
|
||||
new vscode.Range(backwardPosition, backwardPosition.with({ character: backwardPosition.character + 1 })),
|
||||
startCharNew,
|
||||
);
|
||||
});
|
||||
}),
|
||||
|
||||
// implement match delete character
|
||||
parseKeysRegex(/^md(.)$/, /^md/, [Mode.Normal, Mode.Visual], (helixState, editor, match) => {
|
||||
const char = match[1];
|
||||
const [startChar, endChar] = getMatchPairs(char);
|
||||
const num = helixState.resolveCount();
|
||||
|
||||
const forwardPosition = searchForwardBracket(editor.document, startChar, endChar, editor.selection.active, num);
|
||||
const backwardPosition = searchBackwardBracket(editor.document, startChar, endChar, editor.selection.active, num);
|
||||
|
||||
if (forwardPosition === undefined || backwardPosition === undefined) return;
|
||||
|
||||
// Add char to both ends of each selection
|
||||
editor.edit((editBuilder) => {
|
||||
// Add char to both ends of each selection
|
||||
editBuilder.delete(
|
||||
new vscode.Range(forwardPosition, forwardPosition.with({ character: forwardPosition.character + 1 })),
|
||||
);
|
||||
editBuilder.delete(
|
||||
new vscode.Range(backwardPosition, backwardPosition.with({ character: backwardPosition.character + 1 })),
|
||||
);
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
const getMatchPairs = (char: string) => {
|
||||
let startChar: string;
|
||||
let endChar: string;
|
||||
if (['{', '}'].includes(char)) {
|
||||
startChar = '{';
|
||||
endChar = '}';
|
||||
} else if (['[', ']'].includes(char)) {
|
||||
startChar = '[';
|
||||
endChar = ']';
|
||||
} else if (['(', ')'].includes(char)) {
|
||||
startChar = '(';
|
||||
endChar = ')';
|
||||
} else if (['<', '>'].includes(char)) {
|
||||
startChar = '<';
|
||||
endChar = '>';
|
||||
} else {
|
||||
// Otherwise, startChar and endChar should be the same character
|
||||
startChar = char;
|
||||
endChar = char;
|
||||
}
|
||||
|
||||
return [startChar, endChar];
|
||||
};
|
2355
src/actions/motion.ts
Normal file
2355
src/actions/motion.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,486 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { Action } from '../action_types';
|
||||
import { HelixState } from '../helix_state_types';
|
||||
import { Mode } from '../modes_types';
|
||||
import { paragraphBackward, paragraphForward } from '../paragraph_utils';
|
||||
import { parseKeysExact, parseKeysRegex } from '../parse_keys';
|
||||
import * as positionUtils from '../position_utils';
|
||||
import { searchBackward, searchForward } from '../search_utils';
|
||||
import {
|
||||
vimToVscodeVisualLineSelection,
|
||||
vimToVscodeVisualSelection,
|
||||
vscodeToVimVisualLineSelection,
|
||||
vscodeToVimVisualSelection,
|
||||
} from '../selection_utils';
|
||||
import { setVisualLineSelections } from '../visual_line_utils';
|
||||
import { setVisualSelections } from '../visual_utils';
|
||||
import { whitespaceWordRanges, wordRanges } from '../word_utils';
|
||||
import KeyMap from './keymaps';
|
||||
|
||||
export const motions: Action[] = [
|
||||
parseKeysExact([KeyMap.Motions.MoveRight], [Mode.Visual], (vimState, editor) => {
|
||||
execMotion(vimState, editor, ({ document, position }) => {
|
||||
return positionUtils.rightNormal(document, position, vimState.resolveCount());
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact([KeyMap.Motions.MoveLeft], [Mode.Visual], (vimState, editor) => {
|
||||
execMotion(vimState, editor, ({ position }) => {
|
||||
return positionUtils.left(position, vimState.resolveCount());
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact([KeyMap.Motions.MoveRight], [Mode.Normal], () => {
|
||||
vscode.commands.executeCommand('cursorRight');
|
||||
}),
|
||||
|
||||
parseKeysExact([KeyMap.Motions.MoveLeft], [Mode.Normal], () => {
|
||||
vscode.commands.executeCommand('cursorLeft');
|
||||
}),
|
||||
|
||||
parseKeysExact([KeyMap.Motions.MoveUp], [Mode.Normal], (_vimState, _editor) => {
|
||||
vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'up',
|
||||
by: 'wrappedLine',
|
||||
value: _vimState.resolveCount(),
|
||||
});
|
||||
}),
|
||||
parseKeysExact([KeyMap.Motions.MoveUp], [Mode.Visual], (vimState, editor) => {
|
||||
const originalSelections = editor.selections;
|
||||
|
||||
vscode.commands
|
||||
.executeCommand('cursorMove', {
|
||||
to: 'up',
|
||||
by: 'wrappedLine',
|
||||
select: true,
|
||||
value: vimState.resolveCount(),
|
||||
})
|
||||
.then(() => {
|
||||
setVisualSelections(editor, originalSelections);
|
||||
});
|
||||
}),
|
||||
parseKeysExact([KeyMap.Motions.MoveUp], [Mode.VisualLine], (vimState, editor) => {
|
||||
vscode.commands
|
||||
.executeCommand('cursorMove', { to: 'up', by: 'line', select: true, value: vimState.resolveCount() })
|
||||
.then(() => {
|
||||
setVisualLineSelections(editor);
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact([KeyMap.Motions.MoveDown], [Mode.Normal], (_vimState, _editor) => {
|
||||
vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'down',
|
||||
by: 'wrappedLine',
|
||||
value: _vimState.resolveCount(),
|
||||
});
|
||||
}),
|
||||
parseKeysExact([KeyMap.Motions.MoveDown], [Mode.Visual], (vimState, editor) => {
|
||||
const originalSelections = editor.selections;
|
||||
|
||||
vscode.commands
|
||||
.executeCommand('cursorMove', {
|
||||
to: 'down',
|
||||
by: 'wrappedLine',
|
||||
select: true,
|
||||
value: vimState.resolveCount(),
|
||||
})
|
||||
.then(() => {
|
||||
setVisualSelections(editor, originalSelections);
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact([KeyMap.Motions.MoveDown], [Mode.VisualLine], (vimState, editor) => {
|
||||
vscode.commands.executeCommand('cursorMove', { to: 'down', by: 'line', select: true }).then(() => {
|
||||
setVisualLineSelections(editor);
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['w'], [Mode.Normal, Mode.Visual], createWordForwardHandler(wordRanges)),
|
||||
parseKeysExact(['W'], [Mode.Normal, Mode.Visual], createWordForwardHandler(whitespaceWordRanges)),
|
||||
|
||||
parseKeysExact(['b'], [Mode.Normal, Mode.Visual], createWordBackwardHandler(wordRanges)),
|
||||
parseKeysExact(['B'], [Mode.Normal, Mode.Visual], createWordBackwardHandler(whitespaceWordRanges)),
|
||||
|
||||
parseKeysExact(['e'], [Mode.Normal, Mode.Visual], createWordEndHandler(wordRanges)),
|
||||
parseKeysExact(['E'], [Mode.Normal, Mode.Visual], createWordEndHandler(whitespaceWordRanges)),
|
||||
|
||||
parseKeysRegex(/^f(.)$/, /^(f|f.)$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => {
|
||||
findForward(vimState, editor, match);
|
||||
|
||||
vimState.repeatLastMotion = (innerVimState, innerEditor) => {
|
||||
findForward(innerVimState, innerEditor, match);
|
||||
};
|
||||
}),
|
||||
|
||||
parseKeysRegex(/^F(.)$/, /^(F|F.)$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => {
|
||||
findBackward(vimState, editor, match);
|
||||
|
||||
vimState.repeatLastMotion = (innerVimState, innerEditor) => {
|
||||
findBackward(innerVimState, innerEditor, match);
|
||||
};
|
||||
}),
|
||||
|
||||
parseKeysRegex(/^t(.)$/, /^t$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => {
|
||||
tillForward(vimState, editor, match);
|
||||
|
||||
vimState.repeatLastMotion = (innerVimState, innerEditor) => {
|
||||
tillForward(innerVimState, innerEditor, match);
|
||||
};
|
||||
}),
|
||||
|
||||
parseKeysRegex(/^T(.)$/, /^T$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => {
|
||||
tillBackward(vimState, editor, match);
|
||||
|
||||
vimState.repeatLastMotion = (innerVimState, innerEditor) => {
|
||||
tillBackward(innerVimState, innerEditor, match);
|
||||
};
|
||||
}),
|
||||
|
||||
parseKeysExact(['}'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => {
|
||||
execMotion(vimState, editor, ({ document, position }) => {
|
||||
return new vscode.Position(paragraphForward(document, position.line), 0);
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact([']', 'p'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => {
|
||||
execMotion(vimState, editor, ({ document, position }) => {
|
||||
return new vscode.Position(paragraphForward(document, position.line), 0);
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['{'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => {
|
||||
execMotion(vimState, editor, ({ document, position }) => {
|
||||
return new vscode.Position(paragraphBackward(document, position.line), 0);
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['[', 'p'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => {
|
||||
execMotion(vimState, editor, ({ document, position }) => {
|
||||
return new vscode.Position(paragraphBackward(document, position.line), 0);
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact([KeyMap.Motions.MoveLineEnd], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => {
|
||||
execMotion(vimState, editor, ({ document, position }) => {
|
||||
const lineLength = document.lineAt(position.line).text.length;
|
||||
return position.with({ character: Math.max(lineLength - 1, 0) });
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact([KeyMap.Motions.MoveLineStart], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => {
|
||||
execMotion(vimState, editor, ({ document, position }) => {
|
||||
const line = document.lineAt(position.line);
|
||||
return position.with({
|
||||
character: line.firstNonWhitespaceCharacterIndex,
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['H'], [Mode.Normal], (_vimState, _editor) => {
|
||||
vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'viewPortTop',
|
||||
by: 'line',
|
||||
});
|
||||
}),
|
||||
parseKeysExact(['H'], [Mode.Visual], (vimState, editor) => {
|
||||
const originalSelections = editor.selections;
|
||||
|
||||
vscode.commands
|
||||
.executeCommand('cursorMove', {
|
||||
to: 'viewPortTop',
|
||||
by: 'line',
|
||||
select: true,
|
||||
})
|
||||
.then(() => {
|
||||
setVisualSelections(editor, originalSelections);
|
||||
});
|
||||
}),
|
||||
parseKeysExact(['H'], [Mode.VisualLine], (vimState, editor) => {
|
||||
vscode.commands
|
||||
.executeCommand('cursorMove', {
|
||||
to: 'viewPortTop',
|
||||
by: 'line',
|
||||
select: true,
|
||||
})
|
||||
.then(() => {
|
||||
setVisualLineSelections(editor);
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['M'], [Mode.Normal], (_vimState, _editor) => {
|
||||
vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'viewPortCenter',
|
||||
by: 'line',
|
||||
});
|
||||
}),
|
||||
parseKeysExact(['M'], [Mode.Visual], (vimState, editor) => {
|
||||
const originalSelections = editor.selections;
|
||||
|
||||
vscode.commands
|
||||
.executeCommand('cursorMove', {
|
||||
to: 'viewPortCenter',
|
||||
by: 'line',
|
||||
select: true,
|
||||
})
|
||||
.then(() => {
|
||||
setVisualSelections(editor, originalSelections);
|
||||
});
|
||||
}),
|
||||
parseKeysExact(['M'], [Mode.VisualLine], (vimState, editor) => {
|
||||
vscode.commands
|
||||
.executeCommand('cursorMove', {
|
||||
to: 'viewPortCenter',
|
||||
by: 'line',
|
||||
select: true,
|
||||
})
|
||||
.then(() => {
|
||||
setVisualLineSelections(editor);
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['L'], [Mode.Normal], (_vimState, _editor) => {
|
||||
vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'viewPortBottom',
|
||||
by: 'line',
|
||||
});
|
||||
}),
|
||||
parseKeysExact(['L'], [Mode.Visual], (vimState, editor) => {
|
||||
const originalSelections = editor.selections;
|
||||
|
||||
vscode.commands
|
||||
.executeCommand('cursorMove', {
|
||||
to: 'viewPortBottom',
|
||||
by: 'line',
|
||||
select: true,
|
||||
})
|
||||
.then(() => {
|
||||
setVisualSelections(editor, originalSelections);
|
||||
});
|
||||
}),
|
||||
parseKeysExact(['L'], [Mode.VisualLine], (vimState, editor) => {
|
||||
vscode.commands
|
||||
.executeCommand('cursorMove', {
|
||||
to: 'viewPortBottom',
|
||||
by: 'line',
|
||||
select: true,
|
||||
})
|
||||
.then(() => {
|
||||
setVisualLineSelections(editor);
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
type MotionArgs = {
|
||||
document: vscode.TextDocument;
|
||||
position: vscode.Position;
|
||||
selectionIndex: number;
|
||||
vimState: HelixState;
|
||||
};
|
||||
|
||||
type RegexMotionArgs = {
|
||||
document: vscode.TextDocument;
|
||||
position: vscode.Position;
|
||||
selectionIndex: number;
|
||||
vimState: HelixState;
|
||||
match: RegExpMatchArray;
|
||||
};
|
||||
|
||||
function execRegexMotion(
|
||||
vimState: HelixState,
|
||||
editor: vscode.TextEditor,
|
||||
match: RegExpMatchArray,
|
||||
regexMotion: (args: RegexMotionArgs) => vscode.Position,
|
||||
) {
|
||||
return execMotion(vimState, editor, (motionArgs) => {
|
||||
return regexMotion({
|
||||
...motionArgs,
|
||||
match: match,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function execMotion(vimState: HelixState, editor: vscode.TextEditor, motion: (args: MotionArgs) => vscode.Position) {
|
||||
const document = editor.document;
|
||||
|
||||
const newSelections = editor.selections.map((selection, i) => {
|
||||
if (vimState.mode === Mode.Normal) {
|
||||
const newPosition = motion({
|
||||
document: document,
|
||||
position: selection.active,
|
||||
selectionIndex: i,
|
||||
vimState: vimState,
|
||||
});
|
||||
return new vscode.Selection(selection.active, newPosition);
|
||||
} else if (vimState.mode === Mode.Visual) {
|
||||
const vimSelection = vscodeToVimVisualSelection(document, selection);
|
||||
const motionPosition = motion({
|
||||
document: document,
|
||||
position: vimSelection.active,
|
||||
selectionIndex: i,
|
||||
vimState: vimState,
|
||||
});
|
||||
|
||||
return vimToVscodeVisualSelection(document, new vscode.Selection(vimSelection.anchor, motionPosition));
|
||||
} else if (vimState.mode === Mode.VisualLine) {
|
||||
const vimSelection = vscodeToVimVisualLineSelection(document, selection);
|
||||
const motionPosition = motion({
|
||||
document: document,
|
||||
position: vimSelection.active,
|
||||
selectionIndex: i,
|
||||
vimState: vimState,
|
||||
});
|
||||
|
||||
return vimToVscodeVisualLineSelection(document, new vscode.Selection(vimSelection.anchor, motionPosition));
|
||||
} else {
|
||||
return selection;
|
||||
}
|
||||
});
|
||||
|
||||
editor.selections = newSelections;
|
||||
|
||||
editor.revealRange(
|
||||
new vscode.Range(newSelections[0].active, newSelections[0].active),
|
||||
vscode.TextEditorRevealType.InCenterIfOutsideViewport,
|
||||
);
|
||||
}
|
||||
|
||||
function findForward(vimState: HelixState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void {
|
||||
execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => {
|
||||
const fromPosition = position.with({ character: position.character + 1 });
|
||||
const result = searchForward(document, match[1], fromPosition);
|
||||
|
||||
if (result) {
|
||||
return result.with({ character: result.character + 1 });
|
||||
} else {
|
||||
return position;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function findBackward(vimState: HelixState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void {
|
||||
execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => {
|
||||
const fromPosition = positionLeftWrap(document, position);
|
||||
const result = searchBackward(document, match[1], fromPosition);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
} else {
|
||||
return position;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tillForward(vimState: HelixState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void {
|
||||
execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => {
|
||||
const fromPosition = position.with({ character: position.character + 1 });
|
||||
const result = searchForward(document, match[1], fromPosition);
|
||||
|
||||
if (result) {
|
||||
return result.with({ character: result.character });
|
||||
} else {
|
||||
return position;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tillBackward(vimState: HelixState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void {
|
||||
execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => {
|
||||
const fromPosition = positionLeftWrap(document, position);
|
||||
const result = searchBackward(document, match[1], fromPosition);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
} else {
|
||||
return position;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function positionLeftWrap(document: vscode.TextDocument, position: vscode.Position): vscode.Position {
|
||||
if (position.character === 0) {
|
||||
if (position.line === 0) {
|
||||
return position;
|
||||
} else {
|
||||
const lineLength = document.lineAt(position.line - 1).text.length;
|
||||
return new vscode.Position(position.line - 1, lineLength);
|
||||
}
|
||||
} else {
|
||||
return position.with({ character: position.character - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
function createWordForwardHandler(
|
||||
wordRangesFunction: (text: string) => { start: number; end: number }[],
|
||||
): (vimState: HelixState, editor: vscode.TextEditor) => void {
|
||||
return (vimState, editor) => {
|
||||
execMotion(vimState, editor, ({ document, position }) => {
|
||||
let character = position.character;
|
||||
// Try the current line and if we're at the end go to the next line
|
||||
// This way we're only keeping one line of text in memory at a time
|
||||
// i is representing the relative line number we're on from where we started
|
||||
for (let i = 0; i < document.lineCount; i++) {
|
||||
const lineText = document.lineAt(position.line + i).text;
|
||||
const ranges = wordRangesFunction(lineText);
|
||||
|
||||
const result = ranges.find((x) => x.start > character);
|
||||
|
||||
if (result) {
|
||||
return position.with({ character: result.start, line: position.line + i });
|
||||
}
|
||||
// If we don't find anything on this line, search the next and reset the character to 0
|
||||
character = 0;
|
||||
}
|
||||
|
||||
// We may be at the end of the document or nothing else matches
|
||||
return position;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createWordBackwardHandler(
|
||||
wordRangesFunction: (text: string) => { start: number; end: number }[],
|
||||
): (vimState: HelixState, editor: vscode.TextEditor) => void {
|
||||
return (vimState, editor) => {
|
||||
execMotion(vimState, editor, ({ document, position }) => {
|
||||
let character = position.character;
|
||||
// Try the current line and if we're at the end go to the next line
|
||||
// This way we're only keeping one line of text in memory at a time
|
||||
// i is representing the relative line number we're on from where we started
|
||||
for (let i = position.line; i >= 0; i--) {
|
||||
const lineText = document.lineAt(i).text;
|
||||
const ranges = wordRangesFunction(lineText);
|
||||
|
||||
const result = ranges.reverse().find((x) => x.start < character);
|
||||
|
||||
if (result) {
|
||||
return position.with({ character: result.start, line: i });
|
||||
}
|
||||
|
||||
// If we don't find anything on this line, search the next and reset the character to 0
|
||||
character = Infinity;
|
||||
}
|
||||
// We may be at the end of the document or nothing else matches
|
||||
return position;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createWordEndHandler(
|
||||
wordRangesFunction: (text: string) => { start: number; end: number }[],
|
||||
): (vimState: HelixState, editor: vscode.TextEditor) => void {
|
||||
return (vimState, editor) => {
|
||||
execMotion(vimState, editor, ({ document, position }) => {
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
const ranges = wordRangesFunction(lineText);
|
||||
|
||||
const result = ranges.find((x) => x.end > position.character);
|
||||
|
||||
if (result) {
|
||||
return position.with({ character: result.end });
|
||||
} else {
|
||||
return position;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
1110
src/actions/operator.ts
Normal file
1110
src/actions/operator.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,588 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { arrayFindLast } from '../array_utils';
|
||||
import { blockRange } from '../block_utils';
|
||||
import { HelixState } from '../helix_state_types';
|
||||
import { indentLevelRange } from '../indent_utils';
|
||||
import { paragraphBackward, paragraphForward, paragraphRangeInner, paragraphRangeOuter } from '../paragraph_utils';
|
||||
import { createOperatorRangeExactKeys, createOperatorRangeRegex } from '../parse_keys';
|
||||
import { OperatorRange } from '../parse_keys_types';
|
||||
import * as positionUtils from '../position_utils';
|
||||
import { findQuoteRange, quoteRanges } from '../quote_utils';
|
||||
import { searchBackward, searchBackwardBracket, searchForward, searchForwardBracket } from '../search_utils';
|
||||
import { getTags } from '../tag_utils';
|
||||
import { whitespaceWordRanges, wordRanges } from '../word_utils';
|
||||
// import KeyMap from "./keymap";
|
||||
|
||||
export const operatorRanges: OperatorRange[] = [
|
||||
// createOperatorRangeExactKeys(
|
||||
// [KeyMap.Motions.MoveRight],
|
||||
// false,
|
||||
// (vimState, document, position) => {
|
||||
// const right = positionUtils.right(document, position);
|
||||
|
||||
// if (right.isEqual(position)) {
|
||||
// return undefined;
|
||||
// } else {
|
||||
// return new vscode.Range(position, right);
|
||||
// }
|
||||
// }
|
||||
// ),
|
||||
// createOperatorRangeExactKeys(
|
||||
// [KeyMap.Motions.MoveLeft],
|
||||
// false,
|
||||
// (vimState, document, position) => {
|
||||
// const left = positionUtils.left(position);
|
||||
|
||||
// if (left.isEqual(position)) {
|
||||
// return undefined;
|
||||
// } else {
|
||||
// return new vscode.Range(position, left);
|
||||
// }
|
||||
// }
|
||||
// ),
|
||||
// createOperatorRangeExactKeys(
|
||||
// [KeyMap.Motions.MoveUp],
|
||||
// true,
|
||||
// (vimState, document, position) => {
|
||||
// if (position.line === 0) {
|
||||
// return new vscode.Range(
|
||||
// new vscode.Position(0, 0),
|
||||
// positionUtils.lineEnd(document, position)
|
||||
// );
|
||||
// } else {
|
||||
// return new vscode.Range(
|
||||
// new vscode.Position(position.line - 1, 0),
|
||||
// positionUtils.lineEnd(document, position)
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// ),
|
||||
|
||||
// createOperatorRangeExactKeys(
|
||||
// [KeyMap.Motions.MoveDown],
|
||||
// true,
|
||||
// (vimState, document, position) => {
|
||||
// if (position.line === document.lineCount - 1) {
|
||||
// return new vscode.Range(
|
||||
// new vscode.Position(position.line, 0),
|
||||
// positionUtils.lineEnd(document, position)
|
||||
// );
|
||||
// } else {
|
||||
// return new vscode.Range(
|
||||
// new vscode.Position(position.line, 0),
|
||||
// positionUtils.lineEnd(
|
||||
// document,
|
||||
// position.with({ line: position.line + 1 })
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// ),
|
||||
|
||||
createOperatorRangeExactKeys(['w'], false, createWordForwardHandler(wordRanges)),
|
||||
createOperatorRangeExactKeys(['W'], false, createWordForwardHandler(whitespaceWordRanges)),
|
||||
|
||||
createOperatorRangeExactKeys(['b'], false, createWordBackwardHandler(wordRanges)),
|
||||
createOperatorRangeExactKeys(['B'], false, createWordBackwardHandler(whitespaceWordRanges)),
|
||||
|
||||
createOperatorRangeExactKeys(['e'], false, createWordEndHandler(wordRanges)),
|
||||
createOperatorRangeExactKeys(['E'], false, createWordEndHandler(whitespaceWordRanges)),
|
||||
|
||||
createOperatorRangeExactKeys(['i', 'w'], false, createInnerWordHandler(wordRanges)),
|
||||
createOperatorRangeExactKeys(['i', 'W'], false, createInnerWordHandler(whitespaceWordRanges)),
|
||||
|
||||
createOperatorRangeExactKeys(['a', 'w'], false, createOuterWordHandler(wordRanges)),
|
||||
createOperatorRangeExactKeys(['a', 'W'], false, createOuterWordHandler(whitespaceWordRanges)),
|
||||
|
||||
createOperatorRangeRegex(/^f(..)$/, /^(f|f.)$/, false, (vimState, document, position, match) => {
|
||||
const fromPosition = position.with({ character: position.character + 1 });
|
||||
const result = searchForward(document, match[1], fromPosition);
|
||||
|
||||
if (result) {
|
||||
return new vscode.Range(position, result);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
|
||||
createOperatorRangeRegex(/^F(..)$/, /^(F|F.)$/, false, (vimState, document, position, match) => {
|
||||
const fromPosition = position.with({ character: position.character - 1 });
|
||||
const result = searchBackward(document, match[1], fromPosition);
|
||||
|
||||
if (result) {
|
||||
return new vscode.Range(position, result);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
|
||||
createOperatorRangeRegex(/^t(.)$/, /^t$/, false, (vimState, document, position, match) => {
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
const result = lineText.indexOf(match[1], position.character + 1);
|
||||
|
||||
if (result >= 0) {
|
||||
return new vscode.Range(position, position.with({ character: result }));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
|
||||
createOperatorRangeRegex(/^T(.)$/, /^T$/, false, (vimState, document, position, match) => {
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
const result = lineText.lastIndexOf(match[1], position.character - 1);
|
||||
|
||||
if (result >= 0) {
|
||||
const newPosition = positionUtils.right(document, position.with({ character: result }));
|
||||
return new vscode.Range(newPosition, position);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
|
||||
createOperatorRangeExactKeys(['g', 'g'], true, (vimState, document, position) => {
|
||||
const lineLength = document.lineAt(position.line).text.length;
|
||||
|
||||
return new vscode.Range(new vscode.Position(0, 0), position.with({ character: lineLength }));
|
||||
}),
|
||||
|
||||
createOperatorRangeExactKeys(['G'], true, (vimState, document, position) => {
|
||||
const lineLength = document.lineAt(document.lineCount - 1).text.length;
|
||||
|
||||
return new vscode.Range(position.with({ character: 0 }), new vscode.Position(document.lineCount - 1, lineLength));
|
||||
}),
|
||||
|
||||
// TODO: return undefined?
|
||||
createOperatorRangeExactKeys(['}'], true, (vimState, document, position) => {
|
||||
return new vscode.Range(
|
||||
position.with({ character: 0 }),
|
||||
new vscode.Position(paragraphForward(document, position.line), 0),
|
||||
);
|
||||
}),
|
||||
|
||||
// TODO: return undefined?
|
||||
createOperatorRangeExactKeys(['{'], true, (vimState, document, position) => {
|
||||
return new vscode.Range(
|
||||
new vscode.Position(paragraphBackward(document, position.line), 0),
|
||||
position.with({ character: 0 }),
|
||||
);
|
||||
}),
|
||||
|
||||
createOperatorRangeExactKeys(['i', 'p'], true, (vimState, document, position) => {
|
||||
const result = paragraphRangeInner(document, position.line);
|
||||
|
||||
if (result) {
|
||||
return new vscode.Range(
|
||||
new vscode.Position(result.start, 0),
|
||||
new vscode.Position(result.end, document.lineAt(result.end).text.length),
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
|
||||
createOperatorRangeExactKeys(['a', 'p'], true, (vimState, document, position) => {
|
||||
const result = paragraphRangeOuter(document, position.line);
|
||||
|
||||
if (result) {
|
||||
return new vscode.Range(
|
||||
new vscode.Position(result.start, 0),
|
||||
new vscode.Position(result.end, document.lineAt(result.end).text.length),
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
|
||||
createOperatorRangeExactKeys(['i', "'"], false, createInnerQuoteHandler("'")),
|
||||
createOperatorRangeExactKeys(['a', "'"], false, createOuterQuoteHandler("'")),
|
||||
|
||||
createOperatorRangeExactKeys(['i', '"'], false, createInnerQuoteHandler('"')),
|
||||
createOperatorRangeExactKeys(['a', '"'], false, createOuterQuoteHandler('"')),
|
||||
|
||||
createOperatorRangeExactKeys(['i', '`'], false, createInnerQuoteHandler('`')),
|
||||
createOperatorRangeExactKeys(['a', '`'], false, createOuterQuoteHandler('`')),
|
||||
|
||||
createOperatorRangeExactKeys(['i', '('], false, createInnerBracketHandler('(', ')')),
|
||||
createOperatorRangeExactKeys(['a', '('], false, createOuterBracketHandler('(', ')')),
|
||||
|
||||
createOperatorRangeExactKeys(['i', '{'], false, createInnerBracketHandler('{', '}')),
|
||||
createOperatorRangeExactKeys(['a', '{'], false, createOuterBracketHandler('{', '}')),
|
||||
|
||||
createOperatorRangeExactKeys(['i', '['], false, createInnerBracketHandler('[', ']')),
|
||||
createOperatorRangeExactKeys(['a', '['], false, createOuterBracketHandler('[', ']')),
|
||||
|
||||
createOperatorRangeExactKeys(['i', '<'], false, createInnerBracketHandler('<', '>')),
|
||||
createOperatorRangeExactKeys(['a', '<'], false, createOuterBracketHandler('<', '>')),
|
||||
|
||||
createOperatorRangeExactKeys(['i', 'm'], false, createInnerMatchHandler()),
|
||||
createOperatorRangeExactKeys(['a', 'm'], false, createOuterMatchHandler()),
|
||||
|
||||
createOperatorRangeExactKeys(['i', 'f'], false, createInnerFunctionHandler()),
|
||||
createOperatorRangeExactKeys(['a', 'f'], false, createInnerFunctionHandler()),
|
||||
|
||||
createOperatorRangeExactKeys(['i', 't'], false, (vimState, document, position) => {
|
||||
const tags = getTags(document);
|
||||
|
||||
const closestTag = arrayFindLast(tags, (tag) => {
|
||||
if (tag.closing) {
|
||||
return position.isAfterOrEqual(tag.opening.start) && position.isBeforeOrEqual(tag.closing.end);
|
||||
} else {
|
||||
// Self-closing tags have no inside
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (closestTag) {
|
||||
if (closestTag.closing) {
|
||||
return new vscode.Range(
|
||||
closestTag.opening.end.with({
|
||||
character: closestTag.opening.end.character + 1,
|
||||
}),
|
||||
closestTag.closing.start,
|
||||
);
|
||||
} else {
|
||||
throw new Error('We should have already filtered out self-closing tags above');
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
|
||||
createOperatorRangeExactKeys(['a', 't'], false, (vimState, document, position) => {
|
||||
const tags = getTags(document);
|
||||
|
||||
const closestTag = arrayFindLast(tags, (tag) => {
|
||||
const afterStart = position.isAfterOrEqual(tag.opening.start);
|
||||
|
||||
if (tag.closing) {
|
||||
return afterStart && position.isBeforeOrEqual(tag.closing.end);
|
||||
} else {
|
||||
return afterStart && position.isBeforeOrEqual(tag.opening.end);
|
||||
}
|
||||
});
|
||||
|
||||
if (closestTag) {
|
||||
if (closestTag.closing) {
|
||||
return new vscode.Range(
|
||||
closestTag.opening.start,
|
||||
closestTag.closing.end.with({
|
||||
character: closestTag.closing.end.character + 1,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return new vscode.Range(
|
||||
closestTag.opening.start,
|
||||
closestTag.opening.end.with({
|
||||
character: closestTag.opening.end.character + 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
|
||||
// TODO: return undefined?
|
||||
createOperatorRangeExactKeys(['i', 'i'], true, (vimState, document, position) => {
|
||||
const simpleRange = indentLevelRange(document, position.line);
|
||||
|
||||
return new vscode.Range(
|
||||
new vscode.Position(simpleRange.start, 0),
|
||||
new vscode.Position(simpleRange.end, document.lineAt(simpleRange.end).text.length),
|
||||
);
|
||||
}),
|
||||
|
||||
createOperatorRangeExactKeys(['a', 'b'], true, (vimState, document, position) => {
|
||||
const range = blockRange(document, position);
|
||||
|
||||
return range;
|
||||
}),
|
||||
];
|
||||
|
||||
function createInnerBracketHandler(
|
||||
openingChar: string,
|
||||
closingChar: string,
|
||||
): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined {
|
||||
return (helixState, document, position) => {
|
||||
const count = helixState.resolveCount();
|
||||
const bracketRange = getBracketRange(document, position, openingChar, closingChar, count);
|
||||
|
||||
if (bracketRange) {
|
||||
return new vscode.Range(
|
||||
bracketRange.start.with({
|
||||
character: bracketRange.start.character + 1,
|
||||
}),
|
||||
bracketRange.end,
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createOuterBracketHandler(
|
||||
openingChar: string,
|
||||
closingChar: string,
|
||||
): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined {
|
||||
return (helixState, document, position) => {
|
||||
const count = helixState.resolveCount();
|
||||
const bracketRange = getBracketRange(document, position, openingChar, closingChar, count);
|
||||
|
||||
if (bracketRange) {
|
||||
return new vscode.Range(bracketRange.start, bracketRange.end.with({ character: bracketRange.end.character + 1 }));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getBracketRange(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
openingChar: string,
|
||||
closingChar: string,
|
||||
offset?: number,
|
||||
): vscode.Range | undefined {
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
const currentChar = lineText[position.character];
|
||||
|
||||
let start;
|
||||
let end;
|
||||
if (currentChar === openingChar) {
|
||||
start = position;
|
||||
end = searchForwardBracket(document, openingChar, closingChar, positionUtils.rightWrap(document, position), offset);
|
||||
} else if (currentChar === closingChar) {
|
||||
start = searchBackwardBracket(
|
||||
document,
|
||||
openingChar,
|
||||
closingChar,
|
||||
positionUtils.leftWrap(document, position),
|
||||
offset,
|
||||
);
|
||||
end = position;
|
||||
} else {
|
||||
start = searchBackwardBracket(document, openingChar, closingChar, position, offset);
|
||||
end = searchForwardBracket(document, openingChar, closingChar, position, offset);
|
||||
}
|
||||
|
||||
if (start && end) {
|
||||
return new vscode.Range(start, end);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function createInnerQuoteHandler(
|
||||
quoteChar: string,
|
||||
): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined {
|
||||
return (vimState, document, position) => {
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
const ranges = quoteRanges(quoteChar, lineText);
|
||||
const result = findQuoteRange(ranges, position);
|
||||
|
||||
if (result) {
|
||||
return new vscode.Range(position.with({ character: result.start + 1 }), position.with({ character: result.end }));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createOuterQuoteHandler(
|
||||
quoteChar: string,
|
||||
): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined {
|
||||
return (vimState, document, position) => {
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
const ranges = quoteRanges(quoteChar, lineText);
|
||||
const result = findQuoteRange(ranges, position);
|
||||
|
||||
if (result) {
|
||||
return new vscode.Range(position.with({ character: result.start }), position.with({ character: result.end + 1 }));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createWordForwardHandler(
|
||||
wordRangesFunction: (text: string) => { start: number; end: number }[],
|
||||
): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range {
|
||||
return (vimState, document, position) => {
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
const ranges = wordRangesFunction(lineText);
|
||||
|
||||
const result = ranges.find((x) => x.start > position.character);
|
||||
|
||||
if (result) {
|
||||
return new vscode.Range(position, position.with({ character: result.start }));
|
||||
} else {
|
||||
return new vscode.Range(position, position.with({ character: lineText.length }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createWordBackwardHandler(
|
||||
wordRangesFunction: (text: string) => { start: number; end: number }[],
|
||||
): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined {
|
||||
return (vimState, document, position) => {
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
const ranges = wordRangesFunction(lineText);
|
||||
|
||||
const result = ranges.reverse().find((x) => x.start < position.character);
|
||||
|
||||
if (result) {
|
||||
return new vscode.Range(position.with({ character: result.start }), position);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createWordEndHandler(
|
||||
wordRangesFunction: (text: string) => { start: number; end: number }[],
|
||||
): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined {
|
||||
return (vimState, document, position) => {
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
const ranges = wordRangesFunction(lineText);
|
||||
|
||||
const result = ranges.find((x) => x.end > position.character);
|
||||
|
||||
if (result) {
|
||||
return new vscode.Range(position, positionUtils.right(document, position.with({ character: result.end })));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createInnerWordHandler(
|
||||
wordRangesFunction: (text: string) => { start: number; end: number }[],
|
||||
): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined {
|
||||
return (vimState, document, position) => {
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
const ranges = wordRangesFunction(lineText);
|
||||
|
||||
const result = ranges.find((x) => x.start <= position.character && position.character <= x.end);
|
||||
|
||||
if (result) {
|
||||
return new vscode.Range(
|
||||
position.with({ character: result.start }),
|
||||
positionUtils.right(document, position.with({ character: result.end })),
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createOuterWordHandler(
|
||||
wordRangesFunction: (text: string) => { start: number; end: number }[],
|
||||
): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined {
|
||||
return (vimState, document, position) => {
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
const ranges = wordRangesFunction(lineText);
|
||||
|
||||
for (let i = 0; i < ranges.length; ++i) {
|
||||
const range = ranges[i];
|
||||
|
||||
if (range.start <= position.character && position.character <= range.end) {
|
||||
if (i < ranges.length - 1) {
|
||||
return new vscode.Range(
|
||||
position.with({ character: range.start }),
|
||||
position.with({ character: ranges[i + 1].start }),
|
||||
);
|
||||
} else if (i > 0) {
|
||||
return new vscode.Range(
|
||||
positionUtils.right(document, position.with({ character: ranges[i - 1].end })),
|
||||
positionUtils.right(document, position.with({ character: range.end })),
|
||||
);
|
||||
} else {
|
||||
return new vscode.Range(
|
||||
position.with({ character: range.start }),
|
||||
positionUtils.right(document, position.with({ character: range.end })),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Implements going to nearest matching brackets from the cursor.
|
||||
* This will need to call the other `createInnerBracketHandler` functions and get the smallest range from them.
|
||||
* This should ensure that we're fetching the nearest bracket pair.
|
||||
**/
|
||||
function createInnerMatchHandler(): (
|
||||
helixState: HelixState,
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
) => vscode.Range | undefined {
|
||||
return (helixState, document, position) => {
|
||||
const count = helixState.resolveCount();
|
||||
// Get all ranges from our position then reduce down to the shortest one
|
||||
const bracketRange = [
|
||||
getBracketRange(document, position, '(', ')', count),
|
||||
getBracketRange(document, position, '{', '}', count),
|
||||
getBracketRange(document, position, '<', '>', count),
|
||||
getBracketRange(document, position, '[', ']', count),
|
||||
].reduce((acc, range) => {
|
||||
if (range) {
|
||||
if (!acc) {
|
||||
return range;
|
||||
} else {
|
||||
return range.contains(acc) ? acc : range;
|
||||
}
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, undefined);
|
||||
|
||||
return bracketRange?.with(new vscode.Position(bracketRange.start.line, bracketRange.start.character + 1));
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Implements going to nearest matching brackets from the cursor.
|
||||
* This will need to call the other `createInnerBracketHandler` functions and get the smallest range from them.
|
||||
* This should ensure that we're fetching the nearest bracket pair.
|
||||
**/
|
||||
function createOuterMatchHandler(): (
|
||||
vimState: HelixState,
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
) => vscode.Range | undefined {
|
||||
return (_, document, position) => {
|
||||
// Get all ranges from our position then reduce down to the shortest one
|
||||
const bracketRange = [
|
||||
getBracketRange(document, position, '(', ')'),
|
||||
getBracketRange(document, position, '{', '}'),
|
||||
getBracketRange(document, position, '<', '>'),
|
||||
getBracketRange(document, position, '[', ']'),
|
||||
].reduce((acc, range) => {
|
||||
if (range) {
|
||||
if (!acc) {
|
||||
return range;
|
||||
} else {
|
||||
return range.contains(acc) ? acc : range;
|
||||
}
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, undefined);
|
||||
|
||||
return bracketRange?.with(undefined, new vscode.Position(bracketRange.end.line, bracketRange.end.character + 1));
|
||||
};
|
||||
}
|
||||
|
||||
function createInnerFunctionHandler(): (
|
||||
helixState: HelixState,
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
) => vscode.Range | undefined {
|
||||
return (helixState, _, position) => {
|
||||
return helixState.symbolProvider.getContainingSymbolRange(position);
|
||||
};
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { Action } from '../action_types';
|
||||
import { HelixState } from '../helix_state_types';
|
||||
import { enterNormalMode, setModeCursorStyle } from '../modes';
|
||||
import { Mode } from '../modes_types';
|
||||
import { parseKeysOperator } from '../parse_keys';
|
||||
import { operatorRanges } from './operator_ranges';
|
||||
|
||||
export const operators: Action[] = [
|
||||
parseKeysOperator(['d'], operatorRanges, (vimState, editor, ranges, linewise) => {
|
||||
if (ranges.every((x) => x === undefined)) return;
|
||||
|
||||
cursorsToRangesStart(editor, ranges);
|
||||
|
||||
delete_(editor, ranges, linewise);
|
||||
|
||||
if (vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) {
|
||||
enterNormalMode(vimState);
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
}
|
||||
}),
|
||||
|
||||
// Match Mode
|
||||
parseKeysOperator(['m'], operatorRanges, (vimState, editor, ranges) => {
|
||||
if (ranges.every((x) => x === undefined)) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.selections = ranges.map((range, i) => {
|
||||
if (range) {
|
||||
const start = range.start;
|
||||
const end = range.end;
|
||||
return new vscode.Selection(start, end);
|
||||
} else {
|
||||
return editor.selections[i];
|
||||
}
|
||||
});
|
||||
|
||||
setModeCursorStyle(vimState.mode, editor);
|
||||
}),
|
||||
|
||||
parseKeysOperator(['q'], operatorRanges, (vimState, editor, ranges, _linewise) => {
|
||||
if (ranges.every((x) => x === undefined) || vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.selections = ranges.map((range, i) => {
|
||||
if (range) {
|
||||
const start = range.start;
|
||||
const end = range.end;
|
||||
return new vscode.Selection(start, end);
|
||||
} else {
|
||||
return editor.selections[i];
|
||||
}
|
||||
});
|
||||
|
||||
vscode.commands.executeCommand('editor.action.copyLinesDownAction');
|
||||
}),
|
||||
];
|
||||
|
||||
function cursorsToRangesStart(editor: vscode.TextEditor, ranges: readonly (vscode.Range | undefined)[]) {
|
||||
editor.selections = editor.selections.map((selection, i) => {
|
||||
const range = ranges[i];
|
||||
|
||||
if (range) {
|
||||
const newPosition = range.start;
|
||||
return new vscode.Selection(newPosition, newPosition);
|
||||
} else {
|
||||
return selection;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function delete_(editor: vscode.TextEditor, ranges: (vscode.Range | undefined)[], linewise: boolean) {
|
||||
if (ranges.length === 1 && ranges[0] && isEmptyRange(ranges[0])) {
|
||||
vscode.commands.executeCommand('deleteRight');
|
||||
return;
|
||||
}
|
||||
|
||||
editor
|
||||
.edit((editBuilder) => {
|
||||
ranges.forEach((range) => {
|
||||
if (!range) return;
|
||||
|
||||
let deleteRange = range;
|
||||
|
||||
if (linewise) {
|
||||
const start = range.start;
|
||||
const end = range.end;
|
||||
|
||||
if (end.line === editor.document.lineCount - 1) {
|
||||
if (start.line === 0) {
|
||||
deleteRange = new vscode.Range(start.with({ character: 0 }), end);
|
||||
} else {
|
||||
deleteRange = new vscode.Range(
|
||||
new vscode.Position(start.line - 1, editor.document.lineAt(start.line - 1).text.length),
|
||||
end,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
deleteRange = new vscode.Range(range.start, new vscode.Position(end.line + 1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
editBuilder.delete(deleteRange);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// For linewise deletions, make sure cursor is at beginning of line
|
||||
editor.selections = editor.selections.map((selection, i) => {
|
||||
const range = ranges[i];
|
||||
|
||||
if (range && linewise) {
|
||||
const newPosition = selection.start.with({ character: 0 });
|
||||
return new vscode.Selection(newPosition, newPosition);
|
||||
} else {
|
||||
return selection;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function yank(
|
||||
vimState: HelixState,
|
||||
editor: vscode.TextEditor,
|
||||
ranges: (vscode.Range | undefined)[],
|
||||
linewise: boolean,
|
||||
) {
|
||||
vimState.registers = {
|
||||
contentsList: ranges.map((range, i) => {
|
||||
if (range) {
|
||||
return editor.document.getText(range);
|
||||
} else {
|
||||
return vimState.registers.contentsList[i];
|
||||
}
|
||||
}),
|
||||
linewise: linewise,
|
||||
};
|
||||
}
|
||||
|
||||
// detect if a range is covering just a single character
|
||||
function isEmptyRange(range: vscode.Range) {
|
||||
return range.start.line === range.end.line && range.start.character === range.end.character;
|
||||
}
|
||||
|
||||
// detect if the range spans a whole line and only one line
|
||||
// Theres a weird issue where the cursor jumps to the next line when doing expand line selection
|
||||
// https://github.com/microsoft/vscode/issues/118015#issuecomment-854964022
|
||||
export function isSingleLineRange(range: vscode.Range): boolean {
|
||||
return range.start.line === range.end.line && range.start.character === 0 && range.end.character === 0;
|
||||
}
|
133
src/actions/plugins/camelCaseMotion.ts
Normal file
133
src/actions/plugins/camelCaseMotion.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { TextObject } from '../../textobject/textobject';
|
||||
import { RegisterAction } from '../base';
|
||||
import { Mode } from '../../mode/mode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { IMovement, BaseMovement } from '../baseMotion';
|
||||
import { configuration } from '../../configuration/configuration';
|
||||
import { ChangeOperator } from '../operator';
|
||||
import { WordType } from '../../textobject/word';
|
||||
import { Position } from 'vscode';
|
||||
|
||||
abstract class CamelCaseBaseMovement extends BaseMovement {
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]) {
|
||||
return configuration.camelCaseMotion.enable && super.doesActionApply(vimState, keysPressed);
|
||||
}
|
||||
|
||||
public override couldActionApply(vimState: VimState, keysPressed: string[]) {
|
||||
return configuration.camelCaseMotion.enable && super.couldActionApply(vimState, keysPressed);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CamelCaseTextObjectMovement extends TextObject {
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]) {
|
||||
return configuration.camelCaseMotion.enable && super.doesActionApply(vimState, keysPressed);
|
||||
}
|
||||
|
||||
public override couldActionApply(vimState: VimState, keysPressed: string[]) {
|
||||
return configuration.camelCaseMotion.enable && super.couldActionApply(vimState, keysPressed);
|
||||
}
|
||||
}
|
||||
|
||||
// based off of `MoveWordBegin`
|
||||
@RegisterAction
|
||||
class MoveCamelCaseWordBegin extends CamelCaseBaseMovement {
|
||||
keys = ['<leader>', 'w'];
|
||||
|
||||
public override async execAction(position: Position, vimState: VimState): Promise<Position> {
|
||||
if (
|
||||
!configuration.changeWordIncludesWhitespace &&
|
||||
vimState.recordedState.operator instanceof ChangeOperator
|
||||
) {
|
||||
// TODO use execForOperator? Or maybe dont?
|
||||
|
||||
// See note for w
|
||||
return position.nextWordEnd(vimState.document, { wordType: WordType.CamelCase }).getRight();
|
||||
} else {
|
||||
return position.nextWordStart(vimState.document, { wordType: WordType.CamelCase });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// based off of `MoveWordEnd`
|
||||
@RegisterAction
|
||||
class MoveCamelCaseWordEnd extends CamelCaseBaseMovement {
|
||||
keys = ['<leader>', 'e'];
|
||||
|
||||
public override async execAction(position: Position, vimState: VimState): Promise<Position> {
|
||||
return position.nextWordEnd(vimState.document, { wordType: WordType.CamelCase });
|
||||
}
|
||||
|
||||
public override async execActionForOperator(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
): Promise<Position> {
|
||||
const end = position.nextWordEnd(vimState.document, { wordType: WordType.CamelCase });
|
||||
|
||||
return new Position(end.line, end.character + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// based off of `MoveBeginningWord`
|
||||
@RegisterAction
|
||||
class MoveBeginningCamelCaseWord extends CamelCaseBaseMovement {
|
||||
keys = ['<leader>', 'b'];
|
||||
|
||||
public override async execAction(position: Position, vimState: VimState): Promise<Position> {
|
||||
return position.prevWordStart(vimState.document, { wordType: WordType.CamelCase });
|
||||
}
|
||||
}
|
||||
|
||||
// based off of `SelectInnerWord`
|
||||
@RegisterAction
|
||||
class SelectInnerCamelCaseWord extends CamelCaseTextObjectMovement {
|
||||
override modes = [Mode.Normal, Mode.Visual];
|
||||
keys = ['i', '<leader>', 'w'];
|
||||
|
||||
public async execAction(position: Position, vimState: VimState): Promise<IMovement> {
|
||||
let start: Position;
|
||||
let stop: Position;
|
||||
const currentChar = vimState.document.lineAt(position).text[position.character];
|
||||
|
||||
if (/\s/.test(currentChar)) {
|
||||
start = position.prevWordEnd(vimState.document, { wordType: WordType.CamelCase }).getRight();
|
||||
stop = position
|
||||
.nextWordStart(vimState.document, { wordType: WordType.CamelCase })
|
||||
.getLeftThroughLineBreaks();
|
||||
} else {
|
||||
start = position.prevWordStart(vimState.document, {
|
||||
wordType: WordType.CamelCase,
|
||||
inclusive: true,
|
||||
});
|
||||
stop = position.nextWordEnd(vimState.document, {
|
||||
wordType: WordType.CamelCase,
|
||||
inclusive: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
vimState.currentMode === Mode.Visual &&
|
||||
!vimState.cursorStopPosition.isEqual(vimState.cursorStartPosition)
|
||||
) {
|
||||
start = vimState.cursorStartPosition;
|
||||
|
||||
if (vimState.cursorStopPosition.isBefore(vimState.cursorStartPosition)) {
|
||||
// If current cursor postion is before cursor start position, we are selecting words in reverser order.
|
||||
if (/\s/.test(currentChar)) {
|
||||
stop = position
|
||||
.prevWordEnd(vimState.document, { wordType: WordType.CamelCase })
|
||||
.getRight();
|
||||
} else {
|
||||
stop = position.prevWordStart(vimState.document, {
|
||||
wordType: WordType.CamelCase,
|
||||
inclusive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
}
|
||||
}
|
377
src/actions/plugins/easymotion/easymotion.cmd.ts
Normal file
377
src/actions/plugins/easymotion/easymotion.cmd.ts
Normal file
@ -0,0 +1,377 @@
|
||||
import { VimState } from '../../../state/vimState';
|
||||
import { configuration } from './../../../configuration/configuration';
|
||||
import { Mode, isVisualMode } from './../../../mode/mode';
|
||||
import { RegisterAction, BaseCommand } from './../../base';
|
||||
import { EasyMotion } from './easymotion';
|
||||
import {
|
||||
EasyMotionCharMoveOpions,
|
||||
EasyMotionMoveOptionsBase,
|
||||
EasyMotionWordMoveOpions,
|
||||
EasyMotionSearchAction,
|
||||
Match,
|
||||
SearchOptions,
|
||||
} from './types';
|
||||
import { globalState } from '../../../state/globalState';
|
||||
import { TextEditor } from '../../../textEditor';
|
||||
import { MarkerGenerator } from './markerGenerator';
|
||||
import { Position } from 'vscode';
|
||||
|
||||
export interface EasymotionTrigger {
|
||||
key: string;
|
||||
leaderCount?: number;
|
||||
}
|
||||
|
||||
export function buildTriggerKeys(trigger: EasymotionTrigger) {
|
||||
return [
|
||||
...Array.from({ length: trigger.leaderCount || 2 }, () => '<leader>'),
|
||||
...trigger.key.split(''),
|
||||
];
|
||||
}
|
||||
|
||||
abstract class BaseEasyMotionCommand extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
|
||||
private _baseOptions: EasyMotionMoveOptionsBase;
|
||||
|
||||
public abstract getMatches(position: Position, vimState: VimState): Match[];
|
||||
|
||||
constructor(baseOptions: EasyMotionMoveOptionsBase) {
|
||||
super();
|
||||
this._baseOptions = baseOptions;
|
||||
}
|
||||
|
||||
public abstract resolveMatchPosition(match: Match): Position;
|
||||
|
||||
public processMarkers(matches: Match[], cursorPosition: Position, vimState: VimState) {
|
||||
// Clear existing markers, just in case
|
||||
vimState.easyMotion.clearMarkers();
|
||||
|
||||
let index = 0;
|
||||
const markerGenerator = new MarkerGenerator(matches.length);
|
||||
for (const match of matches) {
|
||||
const matchPosition = this.resolveMatchPosition(match);
|
||||
// Skip if the match position equals to cursor position
|
||||
if (!matchPosition.isEqual(cursorPosition)) {
|
||||
const marker = markerGenerator.generateMarker(index++, matchPosition);
|
||||
if (marker) {
|
||||
vimState.easyMotion.addMarker(marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected searchOptions(position: Position): SearchOptions {
|
||||
switch (this._baseOptions.searchOptions) {
|
||||
case 'min':
|
||||
return { min: position };
|
||||
case 'max':
|
||||
return { max: position };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
// Only execute the action if the configuration is set
|
||||
if (configuration.easymotion) {
|
||||
// Search all occurences of the character pressed
|
||||
const matches = this.getMatches(position, vimState);
|
||||
|
||||
// Stop if there are no matches
|
||||
if (matches.length > 0) {
|
||||
vimState.easyMotion = new EasyMotion();
|
||||
this.processMarkers(matches, position, vimState);
|
||||
|
||||
if (matches.length === 1) {
|
||||
// Only one found, navigate to it
|
||||
const marker = vimState.easyMotion.markers[0];
|
||||
// Set cursor position based on marker entered
|
||||
vimState.cursorStopPosition = marker.position;
|
||||
vimState.easyMotion.clearDecorations(vimState.editor);
|
||||
} else {
|
||||
// Store mode to return to after performing easy motion
|
||||
vimState.easyMotion.previousMode = vimState.currentMode;
|
||||
// Enter the EasyMotion mode and await further keys
|
||||
await vimState.setCurrentMode(Mode.EasyMotionMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMatchesForString(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
searchString: string,
|
||||
options?: SearchOptions,
|
||||
): Match[] {
|
||||
switch (searchString) {
|
||||
case '':
|
||||
return [];
|
||||
case ' ':
|
||||
// Searching for space should only find the first space
|
||||
return vimState.easyMotion.sortedSearch(
|
||||
vimState.document,
|
||||
position,
|
||||
new RegExp(' {1,}', 'g'),
|
||||
options,
|
||||
);
|
||||
default:
|
||||
// Search all occurences of the character pressed
|
||||
|
||||
// If the input is not a letter, treating it as regex can cause issues
|
||||
if (!/[a-zA-Z]/.test(searchString)) {
|
||||
return vimState.easyMotion.sortedSearch(vimState.document, position, searchString, options);
|
||||
}
|
||||
|
||||
const ignorecase =
|
||||
configuration.ignorecase && !(configuration.smartcase && /[A-Z]/.test(searchString));
|
||||
const regexFlags = ignorecase ? 'gi' : 'g';
|
||||
return vimState.easyMotion.sortedSearch(
|
||||
vimState.document,
|
||||
position,
|
||||
new RegExp(searchString, regexFlags),
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchByCharCommand extends BaseEasyMotionCommand implements EasyMotionSearchAction {
|
||||
keys = [];
|
||||
public searchString: string = '';
|
||||
private _options: EasyMotionCharMoveOpions;
|
||||
|
||||
get searchCharCount() {
|
||||
return this._options.charCount;
|
||||
}
|
||||
|
||||
constructor(options: EasyMotionCharMoveOpions) {
|
||||
super(options);
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
public getMatches(position: Position, vimState: VimState): Match[] {
|
||||
return getMatchesForString(position, vimState, this.searchString, this.searchOptions(position));
|
||||
}
|
||||
|
||||
public shouldFire() {
|
||||
const charCount = this._options.charCount;
|
||||
return charCount ? this.searchString.length >= charCount : true;
|
||||
}
|
||||
|
||||
public async fire(position: Position, vimState: VimState): Promise<void> {
|
||||
await this.exec(position, vimState);
|
||||
}
|
||||
|
||||
public resolveMatchPosition(match: Match): Position {
|
||||
const { line, character } = match.position;
|
||||
switch (this._options.labelPosition) {
|
||||
case 'after':
|
||||
return new Position(line, character + this._options.charCount);
|
||||
case 'before':
|
||||
return new Position(line, Math.max(0, character - 1));
|
||||
default:
|
||||
return match.position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchByNCharCommand extends BaseEasyMotionCommand implements EasyMotionSearchAction {
|
||||
keys = [];
|
||||
public searchString: string = '';
|
||||
|
||||
get searchCharCount() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super({});
|
||||
}
|
||||
|
||||
public resolveMatchPosition(match: Match): Position {
|
||||
return match.position;
|
||||
}
|
||||
|
||||
public getMatches(position: Position, vimState: VimState): Match[] {
|
||||
return getMatchesForString(
|
||||
position,
|
||||
vimState,
|
||||
this.removeTrailingLineBreak(this.searchString),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
private removeTrailingLineBreak(s: string) {
|
||||
return s.replace(new RegExp('\n+$', 'g'), '');
|
||||
}
|
||||
|
||||
public shouldFire() {
|
||||
// Fire when <CR> typed
|
||||
return this.searchString.endsWith('\n');
|
||||
}
|
||||
|
||||
public async fire(position: Position, vimState: VimState): Promise<void> {
|
||||
if (this.removeTrailingLineBreak(this.searchString) !== '') {
|
||||
await this.exec(position, vimState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class EasyMotionCharMoveCommandBase extends BaseCommand {
|
||||
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
|
||||
private _action: EasyMotionSearchAction;
|
||||
|
||||
constructor(action: EasyMotionSearchAction) {
|
||||
super();
|
||||
this._action = action;
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
// Only execute the action if easymotion is enabled
|
||||
if (configuration.easymotion) {
|
||||
vimState.easyMotion = new EasyMotion();
|
||||
vimState.easyMotion.previousMode = vimState.currentMode;
|
||||
vimState.easyMotion.searchAction = this._action;
|
||||
globalState.hl = true;
|
||||
|
||||
await vimState.setCurrentMode(Mode.EasyMotionInputMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class EasyMotionWordMoveCommandBase extends BaseEasyMotionCommand {
|
||||
private _options: EasyMotionWordMoveOpions;
|
||||
|
||||
constructor(options: EasyMotionWordMoveOpions = {}) {
|
||||
super(options);
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
public getMatches(position: Position, vimState: VimState): Match[] {
|
||||
return this.getMatchesForWord(position, vimState, this.searchOptions(position));
|
||||
}
|
||||
|
||||
public resolveMatchPosition(match: Match): Position {
|
||||
const { line, character } = match.position;
|
||||
switch (this._options.labelPosition) {
|
||||
case 'after':
|
||||
return new Position(line, character + match.text.length - 1);
|
||||
default:
|
||||
return match.position;
|
||||
}
|
||||
}
|
||||
|
||||
private getMatchesForWord(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
options?: SearchOptions,
|
||||
): Match[] {
|
||||
const regex = this._options.jumpToAnywhere
|
||||
? new RegExp(configuration.easymotionJumpToAnywhereRegex, 'g')
|
||||
: new RegExp('\\w{1,}', 'g');
|
||||
return vimState.easyMotion.sortedSearch(vimState.document, position, regex, options);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class EasyMotionLineMoveCommandBase extends BaseEasyMotionCommand {
|
||||
private _options: EasyMotionMoveOptionsBase;
|
||||
|
||||
constructor(options: EasyMotionMoveOptionsBase = {}) {
|
||||
super(options);
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
public resolveMatchPosition(match: Match): Position {
|
||||
return match.position;
|
||||
}
|
||||
|
||||
public getMatches(position: Position, vimState: VimState): Match[] {
|
||||
return this.getMatchesForLineStart(position, vimState, this.searchOptions(position));
|
||||
}
|
||||
|
||||
private getMatchesForLineStart(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
options?: SearchOptions,
|
||||
): Match[] {
|
||||
// Search for the beginning of all non whitespace chars on each line before the cursor
|
||||
const matches = vimState.easyMotion.sortedSearch(
|
||||
vimState.document,
|
||||
position,
|
||||
new RegExp('^.', 'gm'),
|
||||
options,
|
||||
);
|
||||
for (const match of matches) {
|
||||
match.position = TextEditor.getFirstNonWhitespaceCharOnLine(
|
||||
vimState.document,
|
||||
match.position.line,
|
||||
);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionCharInputMode extends BaseCommand {
|
||||
modes = [Mode.EasyMotionInputMode];
|
||||
keys = ['<character>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const key = this.keysPressed[0];
|
||||
const action = vimState.easyMotion.searchAction;
|
||||
action.searchString =
|
||||
key === '<BS>' || key === '<S-BS>'
|
||||
? action.searchString.slice(0, -1)
|
||||
: action.searchString + key;
|
||||
if (action.shouldFire()) {
|
||||
// Skip Easymotion input mode to make sure not to back to it
|
||||
await vimState.setCurrentMode(vimState.easyMotion.previousMode);
|
||||
await action.fire(vimState.cursorStopPosition, vimState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandEscEasyMotionCharInputMode extends BaseCommand {
|
||||
modes = [Mode.EasyMotionInputMode];
|
||||
keys = ['<Esc>'];
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveEasyMotion extends BaseCommand {
|
||||
modes = [Mode.EasyMotionMode];
|
||||
keys = ['<character>'];
|
||||
override isJump = true;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const key = this.keysPressed[0];
|
||||
if (key) {
|
||||
// "nail" refers to the accumulated depth keys
|
||||
const nail = vimState.easyMotion.accumulation + key;
|
||||
vimState.easyMotion.accumulation = nail;
|
||||
|
||||
// Find markers starting with "nail"
|
||||
const markers = vimState.easyMotion.findMarkers(nail, true);
|
||||
|
||||
if (markers.length === 1) {
|
||||
// Only one found, navigate to it
|
||||
const marker = markers[0];
|
||||
|
||||
vimState.easyMotion.clearDecorations(vimState.editor);
|
||||
// Restore the mode from before easy motion
|
||||
await vimState.setCurrentMode(vimState.easyMotion.previousMode);
|
||||
|
||||
// Set cursor position based on marker entered
|
||||
vimState.cursorStopPosition = marker.position;
|
||||
} else if (markers.length === 0) {
|
||||
// None found, exit mode
|
||||
vimState.easyMotion.clearDecorations(vimState.editor);
|
||||
await vimState.setCurrentMode(vimState.easyMotion.previousMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
427
src/actions/plugins/easymotion/easymotion.ts
Normal file
427
src/actions/plugins/easymotion/easymotion.ts
Normal file
@ -0,0 +1,427 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { Position } from 'vscode';
|
||||
import { Mode } from '../../../mode/mode';
|
||||
import { configuration } from './../../../configuration/configuration';
|
||||
import { TextEditor } from './../../../textEditor';
|
||||
import { EasyMotionSearchAction, IEasyMotion, Marker, Match, SearchOptions } from './types';
|
||||
|
||||
export class EasyMotion implements IEasyMotion {
|
||||
/**
|
||||
* Refers to the accumulated keys for depth navigation
|
||||
*/
|
||||
public accumulation = '';
|
||||
|
||||
// TODO: is this actually always set?
|
||||
public searchAction!: EasyMotionSearchAction;
|
||||
|
||||
/**
|
||||
* Array of all markers and decorations
|
||||
*/
|
||||
public readonly markers: Marker[];
|
||||
|
||||
private visibleMarkers: Marker[]; // Array of currently showing markers
|
||||
private decorations: vscode.DecorationOptions[][];
|
||||
|
||||
private static readonly fade = vscode.window.createTextEditorDecorationType({
|
||||
color: configuration.easymotionDimColor,
|
||||
});
|
||||
private static readonly hide = vscode.window.createTextEditorDecorationType({
|
||||
color: 'transparent',
|
||||
});
|
||||
|
||||
/**
|
||||
* TODO: For future motions
|
||||
*/
|
||||
private static specialCharactersRegex: RegExp = /[\-\[\]{}()*+?.,\\\^$|#\s]/g;
|
||||
|
||||
/**
|
||||
* Caches for decorations
|
||||
*/
|
||||
private static decorationTypeCache: vscode.TextEditorDecorationType[] = [];
|
||||
|
||||
/**
|
||||
* Mode to return to after attempting easymotion
|
||||
*/
|
||||
// TODO: make this optional (in some circumstances it isn't actually set)
|
||||
public previousMode!: Mode;
|
||||
|
||||
constructor() {
|
||||
this.markers = [];
|
||||
this.visibleMarkers = [];
|
||||
this.decorations = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and cache decoration types for different marker lengths
|
||||
*/
|
||||
public static getDecorationType(
|
||||
length: number,
|
||||
decorations?: vscode.DecorationRenderOptions,
|
||||
): vscode.TextEditorDecorationType {
|
||||
const cache = this.decorationTypeCache[length];
|
||||
if (cache) {
|
||||
return cache;
|
||||
} else {
|
||||
const type = vscode.window.createTextEditorDecorationType(decorations || {});
|
||||
|
||||
this.decorationTypeCache[length] = type;
|
||||
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all decorations
|
||||
*/
|
||||
public clearDecorations(editor: vscode.TextEditor) {
|
||||
for (let i = 1; i <= this.decorations.length; i++) {
|
||||
editor.setDecorations(EasyMotion.getDecorationType(i), []);
|
||||
}
|
||||
|
||||
editor.setDecorations(EasyMotion.fade, []);
|
||||
editor.setDecorations(EasyMotion.hide, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all markers
|
||||
*/
|
||||
public clearMarkers() {
|
||||
while (this.markers.length) {
|
||||
this.markers.pop();
|
||||
}
|
||||
this.visibleMarkers = [];
|
||||
}
|
||||
|
||||
public addMarker(marker: Marker) {
|
||||
this.markers.push(marker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find markers beginning with a string
|
||||
*/
|
||||
public findMarkers(nail: string, onlyVisible: boolean): Marker[] {
|
||||
const markers = onlyVisible ? this.visibleMarkers : this.markers;
|
||||
return markers.filter((marker) => marker.name.startsWith(nail));
|
||||
}
|
||||
|
||||
/**
|
||||
* Search and sort using the index of a match compared to the index of position (usually the cursor)
|
||||
*/
|
||||
public sortedSearch(
|
||||
document: vscode.TextDocument,
|
||||
position: Position,
|
||||
search: string | RegExp = '',
|
||||
options: SearchOptions = {},
|
||||
): Match[] {
|
||||
const regex =
|
||||
typeof search === 'string'
|
||||
? new RegExp(search.replace(EasyMotion.specialCharactersRegex, '\\$&'), 'g')
|
||||
: search;
|
||||
|
||||
const matches: Match[] = [];
|
||||
|
||||
// Cursor index refers to the index of the marker that is on or to the right of the cursor
|
||||
let cursorIndex = position.character;
|
||||
let prevMatch: Match | undefined;
|
||||
|
||||
// Calculate the min/max bounds for the search
|
||||
const lineCount = document.lineCount;
|
||||
const lineMin = options.min ? Math.max(options.min.line, 0) : 0;
|
||||
const lineMax = options.max ? Math.min(options.max.line + 1, lineCount) : lineCount;
|
||||
|
||||
outer: for (let lineIdx = lineMin; lineIdx < lineMax; lineIdx++) {
|
||||
const line = document.lineAt(lineIdx).text;
|
||||
let result = regex.exec(line);
|
||||
|
||||
while (result) {
|
||||
if (matches.length >= 1000) {
|
||||
break outer;
|
||||
} else {
|
||||
const pos = new Position(lineIdx, result.index);
|
||||
|
||||
// Check if match is within bounds
|
||||
if (
|
||||
(options.min && pos.isBefore(options.min)) ||
|
||||
(options.max && pos.isAfter(options.max)) ||
|
||||
Math.abs(pos.line - position.line) > 100
|
||||
) {
|
||||
// Stop searching after 100 lines in both directions
|
||||
result = regex.exec(line);
|
||||
} else {
|
||||
// Update cursor index to the marker on the right side of the cursor
|
||||
if (!prevMatch || prevMatch.position.isBefore(position)) {
|
||||
cursorIndex = matches.length;
|
||||
}
|
||||
// Matches on the cursor position should be ignored
|
||||
if (pos.isEqual(position)) {
|
||||
result = regex.exec(line);
|
||||
} else {
|
||||
prevMatch = new Match(pos, result[0], matches.length);
|
||||
matches.push(prevMatch);
|
||||
result = regex.exec(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by the index distance from the cursor index
|
||||
matches.sort((a: Match, b: Match): number => {
|
||||
const computeAboluteDiff = (matchIndex: number) => {
|
||||
const absDiff = Math.abs(cursorIndex - matchIndex);
|
||||
// Prioritize the matches on the right side of the cursor index
|
||||
return matchIndex < cursorIndex ? absDiff - 0.5 : absDiff;
|
||||
};
|
||||
|
||||
const absDiffA = computeAboluteDiff(a.index);
|
||||
const absDiffB = computeAboluteDiff(b.index);
|
||||
return absDiffA - absDiffB;
|
||||
});
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private getMarkerColor(
|
||||
customizedValue: string,
|
||||
themeColorId: string,
|
||||
): string | vscode.ThemeColor {
|
||||
if (customizedValue) {
|
||||
return customizedValue;
|
||||
} else if (!themeColorId.startsWith('#')) {
|
||||
return new vscode.ThemeColor(themeColorId);
|
||||
} else {
|
||||
return themeColorId;
|
||||
}
|
||||
}
|
||||
|
||||
private getEasymotionMarkerBackgroundColor() {
|
||||
return this.getMarkerColor(configuration.easymotionMarkerBackgroundColor, '#0000');
|
||||
}
|
||||
|
||||
private getEasymotionMarkerForegroundColorOneChar() {
|
||||
return this.getMarkerColor(configuration.easymotionMarkerForegroundColorOneChar, '#ff0000');
|
||||
}
|
||||
|
||||
private getEasymotionMarkerForegroundColorTwoCharFirst() {
|
||||
return this.getMarkerColor(
|
||||
configuration.easymotionMarkerForegroundColorTwoCharFirst,
|
||||
'#ffb400',
|
||||
);
|
||||
}
|
||||
|
||||
private getEasymotionMarkerForegroundColorTwoCharSecond() {
|
||||
return this.getMarkerColor(
|
||||
configuration.easymotionMarkerForegroundColorTwoCharSecond,
|
||||
'#b98300',
|
||||
);
|
||||
}
|
||||
|
||||
private getEasymotionDimColor() {
|
||||
return this.getMarkerColor(configuration.easymotionDimColor, '#777777');
|
||||
}
|
||||
|
||||
public updateDecorations(editor: vscode.TextEditor) {
|
||||
this.clearDecorations(editor);
|
||||
|
||||
this.visibleMarkers = [];
|
||||
this.decorations = [];
|
||||
|
||||
// Set the decorations for all the different marker lengths
|
||||
const dimmingZones: vscode.DecorationOptions[] = [];
|
||||
const dimmingRenderOptions: vscode.ThemableDecorationRenderOptions = {
|
||||
// we update the color here again in case the configuration has changed
|
||||
color: this.getEasymotionDimColor(),
|
||||
};
|
||||
// Why this instead of `background-color` on the marker?
|
||||
// The easy fix would've been to let the user set the marker background to the same
|
||||
// color as the editor so it would hide the character behind, However this would require
|
||||
// the user to do more work, with this solution we temporarily hide the marked character
|
||||
// so no user specific setting is needed
|
||||
const hiddenChars: vscode.Range[] = [];
|
||||
const markers = this.markers
|
||||
.filter((m) => m.name.startsWith(this.accumulation))
|
||||
.sort((a, b) => (a.position.isBefore(b.position) ? -1 : 1));
|
||||
|
||||
// Ignore markers that do not start with the accumulated depth level
|
||||
for (const marker of markers) {
|
||||
const pos = marker.position;
|
||||
// Get keys after the depth we're at
|
||||
const keystroke = marker.name.substr(this.accumulation.length);
|
||||
|
||||
if (!this.decorations[keystroke.length]) {
|
||||
this.decorations[keystroke.length] = [];
|
||||
}
|
||||
|
||||
// #region Hack (remove once backend handles this)
|
||||
|
||||
/*
|
||||
This hack is here because the backend for easy motion reports two adjacent
|
||||
2 char markers resulting in a 4 char wide markers, this isn't what happens in
|
||||
original easymotion for instance: for doom
|
||||
- original reports d[m][m2]m where [m] is a marker and [m2] is secondary
|
||||
- here it reports d[m][m][m][m]m
|
||||
The reason this won't work with current impl is that it overflows resulting in
|
||||
one extra hidden character, hence the check below (until backend truely mimics original)
|
||||
|
||||
if two consecutive 2 char markers, we only use the first char from the current marker
|
||||
and reduce the char substitution by 1. Once backend properly reports adjacent markers
|
||||
all instances of `trim` can be removed
|
||||
*/
|
||||
let trim = 0;
|
||||
const next = markers[markers.indexOf(marker) + 1];
|
||||
|
||||
if (
|
||||
next &&
|
||||
next.position.character - pos.character === 1 &&
|
||||
next.position.line === pos.line
|
||||
) {
|
||||
const nextKeystroke = next.name.substr(this.accumulation.length);
|
||||
|
||||
if (keystroke.length > 1 && nextKeystroke.length > 1) {
|
||||
trim = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// First Char/One Char decoration
|
||||
const firstCharFontColor =
|
||||
keystroke.length > 1
|
||||
? this.getEasymotionMarkerForegroundColorTwoCharFirst()
|
||||
: this.getEasymotionMarkerForegroundColorOneChar();
|
||||
const backgroundColor = this.getEasymotionMarkerBackgroundColor();
|
||||
const firstCharRange = new vscode.Range(pos.line, pos.character, pos.line, pos.character);
|
||||
const firstCharRenderOptions: vscode.ThemableDecorationInstanceRenderOptions = {
|
||||
before: {
|
||||
contentText: keystroke.substring(0, 1),
|
||||
backgroundColor,
|
||||
color: firstCharFontColor,
|
||||
margin: `0 -1ch 0 0;
|
||||
position: absolute;
|
||||
font-weight: ${configuration.easymotionMarkerFontWeight};`,
|
||||
height: '100%',
|
||||
},
|
||||
};
|
||||
|
||||
this.decorations[keystroke.length].push({
|
||||
range: firstCharRange,
|
||||
renderOptions: {
|
||||
dark: firstCharRenderOptions,
|
||||
light: firstCharRenderOptions,
|
||||
},
|
||||
});
|
||||
|
||||
// Second Char decoration
|
||||
if (keystroke.length + trim > 1) {
|
||||
const secondCharFontColor = this.getEasymotionMarkerForegroundColorTwoCharSecond();
|
||||
const secondCharRange = new vscode.Range(
|
||||
pos.line,
|
||||
pos.character + 1,
|
||||
pos.line,
|
||||
pos.character + 1,
|
||||
);
|
||||
|
||||
const secondCharRenderOptions: vscode.ThemableDecorationInstanceRenderOptions = {
|
||||
before: {
|
||||
contentText: keystroke.slice(1),
|
||||
backgroundColor,
|
||||
color: secondCharFontColor,
|
||||
margin: `0 -1ch 0 0;
|
||||
position: absolute;
|
||||
font-weight: ${configuration.easymotionMarkerFontWeight};`,
|
||||
height: '100%',
|
||||
},
|
||||
};
|
||||
this.decorations[keystroke.length].push({
|
||||
range: secondCharRange,
|
||||
renderOptions: {
|
||||
dark: secondCharRenderOptions,
|
||||
light: secondCharRenderOptions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
hiddenChars.push(
|
||||
new vscode.Range(
|
||||
pos.line,
|
||||
pos.character,
|
||||
pos.line,
|
||||
pos.character + keystroke.length + trim,
|
||||
),
|
||||
);
|
||||
|
||||
if (configuration.easymotionDimBackground) {
|
||||
// This excludes markers from the dimming ranges by using them as anchors
|
||||
// each marker adds the range between it and previous marker to the dimming zone
|
||||
// except last marker after which the rest of document is dimmed
|
||||
//
|
||||
// example [m1] text that has multiple [m2] marks
|
||||
// |<------ |<---------------------- ---->|
|
||||
if (dimmingZones.length === 0) {
|
||||
dimmingZones.push({
|
||||
range: new vscode.Range(0, 0, pos.line, pos.character),
|
||||
renderOptions: dimmingRenderOptions,
|
||||
});
|
||||
} else {
|
||||
const prevMarker = markers[markers.indexOf(marker) - 1];
|
||||
const prevKeystroke = prevMarker.name.substring(this.accumulation.length);
|
||||
const prevDimPos = prevMarker.position;
|
||||
const offsetPrevDimPos = prevDimPos.withColumn(
|
||||
prevDimPos.character + prevKeystroke.length,
|
||||
);
|
||||
|
||||
// Don't create dimming ranges in between consecutive markers (the 'after' is in the cases
|
||||
// where you have 2 char consecutive markers where the first one only shows the first char.
|
||||
// since we don't take that into account when creating 'offsetPrevDimPos' it will be after
|
||||
// the current marker position which means we are in the middle of two consecutive markers.
|
||||
// See the hack region above.)
|
||||
if (!offsetPrevDimPos.isAfterOrEqual(pos)) {
|
||||
dimmingZones.push({
|
||||
range: new vscode.Range(
|
||||
offsetPrevDimPos.line,
|
||||
offsetPrevDimPos.character,
|
||||
pos.line,
|
||||
pos.character,
|
||||
),
|
||||
renderOptions: dimmingRenderOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.visibleMarkers.push(marker);
|
||||
}
|
||||
|
||||
// for the last marker dim till document end
|
||||
if (configuration.easymotionDimBackground && markers.length > 0) {
|
||||
const prevMarker = markers[markers.length - 1];
|
||||
const prevKeystroke = prevMarker.name.substring(this.accumulation.length);
|
||||
const prevDimPos = dimmingZones[dimmingZones.length - 1].range.end;
|
||||
const offsetPrevDimPos = prevDimPos.withColumn(prevDimPos.character + prevKeystroke.length);
|
||||
|
||||
// Don't create any more dimming ranges when the last marker is at document end
|
||||
if (!offsetPrevDimPos.isEqual(TextEditor.getDocumentEnd(editor.document))) {
|
||||
dimmingZones.push({
|
||||
range: new vscode.Range(
|
||||
offsetPrevDimPos,
|
||||
new Position(editor.document.lineCount, Number.MAX_VALUE),
|
||||
),
|
||||
renderOptions: dimmingRenderOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = 1; j < this.decorations.length; j++) {
|
||||
if (this.decorations[j]) {
|
||||
editor.setDecorations(EasyMotion.getDecorationType(j), this.decorations[j]);
|
||||
}
|
||||
}
|
||||
|
||||
editor.setDecorations(EasyMotion.hide, hiddenChars);
|
||||
|
||||
if (configuration.easymotionDimBackground) {
|
||||
editor.setDecorations(EasyMotion.fade, dimmingZones);
|
||||
}
|
||||
}
|
||||
}
|
58
src/actions/plugins/easymotion/markerGenerator.ts
Normal file
58
src/actions/plugins/easymotion/markerGenerator.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { Position } from 'vscode';
|
||||
import { configuration } from './../../../configuration/configuration';
|
||||
import { Marker } from './types';
|
||||
|
||||
export class MarkerGenerator {
|
||||
private matchesCount: number;
|
||||
private keyTable: string[];
|
||||
private prefixKeyTable: string[];
|
||||
|
||||
constructor(matchesCount: number) {
|
||||
this.matchesCount = matchesCount;
|
||||
this.keyTable = this.getKeyTable();
|
||||
this.prefixKeyTable = this.createPrefixKeyTable();
|
||||
}
|
||||
|
||||
public generateMarker(index: number, markerPosition: Position): Marker | null {
|
||||
const { keyTable, prefixKeyTable } = this;
|
||||
|
||||
if (index >= keyTable.length - prefixKeyTable.length) {
|
||||
const remainder = index - (keyTable.length - prefixKeyTable.length);
|
||||
const currentStep = Math.floor(remainder / keyTable.length) + 1;
|
||||
if (currentStep > prefixKeyTable.length) {
|
||||
return null;
|
||||
} else {
|
||||
const prefix = prefixKeyTable[currentStep - 1];
|
||||
const label = keyTable[remainder % keyTable.length];
|
||||
return {
|
||||
name: prefix + label,
|
||||
position: markerPosition,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
name: keyTable[index],
|
||||
position: markerPosition,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private createPrefixKeyTable(): string[] {
|
||||
const totalRemainder = Math.max(this.matchesCount - this.keyTable.length, 0);
|
||||
const totalSteps = Math.ceil(totalRemainder / this.keyTable.length);
|
||||
const reversed = this.keyTable.slice().reverse();
|
||||
const count = Math.min(totalSteps, reversed.length);
|
||||
return reversed.slice(0, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* The key sequence for marker name generation
|
||||
*/
|
||||
private getKeyTable(): string[] {
|
||||
if (configuration.easymotionKeys) {
|
||||
return configuration.easymotionKeys.split('');
|
||||
} else {
|
||||
return 'hklyuiopnm,qwertzxcvbasdgjf;'.split('');
|
||||
}
|
||||
}
|
||||
}
|
246
src/actions/plugins/easymotion/registerMoveActions.ts
Normal file
246
src/actions/plugins/easymotion/registerMoveActions.ts
Normal file
@ -0,0 +1,246 @@
|
||||
import { RegisterAction } from './../../base';
|
||||
import {
|
||||
buildTriggerKeys,
|
||||
EasyMotionCharMoveCommandBase,
|
||||
EasyMotionLineMoveCommandBase,
|
||||
EasyMotionWordMoveCommandBase,
|
||||
SearchByCharCommand,
|
||||
SearchByNCharCommand,
|
||||
} from './easymotion.cmd';
|
||||
|
||||
// EasyMotion n-char-move command
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionNCharSearchCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: '/' });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByNCharCommand());
|
||||
}
|
||||
}
|
||||
|
||||
// EasyMotion char-move commands
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionTwoCharSearchCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: '2s' });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 2 }));
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionTwoCharFindForwardCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: '2f' });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 2, searchOptions: 'min' }));
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionTwoCharFindBackwardCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: '2F' });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 2, searchOptions: 'max' }));
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionTwoCharTilCharacterForwardCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: '2t' });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 2, searchOptions: 'min', labelPosition: 'before' }));
|
||||
}
|
||||
}
|
||||
|
||||
// easymotion-bd-t2
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionTwoCharTilCharacterBidirectionalCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'bd2t', leaderCount: 3 });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 2, labelPosition: 'before' }));
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionTwoCharTilBackwardCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: '2T' });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 2, searchOptions: 'max', labelPosition: 'after' }));
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionSearchCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 's' });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 1 }));
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionFindForwardCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'f' });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 1, searchOptions: 'min' }));
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionFindBackwardCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'F' });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 1, searchOptions: 'max' }));
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionTilCharacterForwardCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 't' });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 1, searchOptions: 'min', labelPosition: 'before' }));
|
||||
}
|
||||
}
|
||||
|
||||
// easymotion-bd-t
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionTilCharacterBidirectionalCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'bdt', leaderCount: 3 });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 1, labelPosition: 'before' }));
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionTilBackwardCommand extends EasyMotionCharMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'T' });
|
||||
|
||||
constructor() {
|
||||
super(new SearchByCharCommand({ charCount: 1, searchOptions: 'max', labelPosition: 'after' }));
|
||||
}
|
||||
}
|
||||
|
||||
// EasyMotion word-move commands
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionStartOfWordForwardsCommand extends EasyMotionWordMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'w' });
|
||||
|
||||
constructor() {
|
||||
super({ searchOptions: 'min' });
|
||||
}
|
||||
}
|
||||
|
||||
// easymotion-bd-w
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionStartOfWordBidirectionalCommand extends EasyMotionWordMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'bdw', leaderCount: 3 });
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionLineForward extends EasyMotionWordMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'l' });
|
||||
|
||||
constructor() {
|
||||
super({ jumpToAnywhere: true, searchOptions: 'min', labelPosition: 'after' });
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionLineBackward extends EasyMotionWordMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'h' });
|
||||
|
||||
constructor() {
|
||||
super({ jumpToAnywhere: true, searchOptions: 'max', labelPosition: 'after' });
|
||||
}
|
||||
}
|
||||
|
||||
// easymotion "JumpToAnywhere" motion
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionJumpToAnywhereCommand extends EasyMotionWordMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'j', leaderCount: 3 });
|
||||
|
||||
constructor() {
|
||||
super({ jumpToAnywhere: true, labelPosition: 'after' });
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionEndOfWordForwardsCommand extends EasyMotionWordMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'e' });
|
||||
|
||||
constructor() {
|
||||
super({ searchOptions: 'min', labelPosition: 'after' });
|
||||
}
|
||||
}
|
||||
|
||||
// easymotion-bd-e
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionEndOfWordBidirectionalCommand extends EasyMotionWordMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'bde', leaderCount: 3 });
|
||||
|
||||
constructor() {
|
||||
super({ labelPosition: 'after' });
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionBeginningWordCommand extends EasyMotionWordMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'b' });
|
||||
|
||||
constructor() {
|
||||
super({ searchOptions: 'max' });
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionEndBackwardCommand extends EasyMotionWordMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'ge' });
|
||||
|
||||
constructor() {
|
||||
super({ searchOptions: 'max', labelPosition: 'after' });
|
||||
}
|
||||
}
|
||||
|
||||
// EasyMotion line-move commands
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionStartOfLineForwardsCommand extends EasyMotionLineMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'j' });
|
||||
|
||||
constructor() {
|
||||
super({ searchOptions: 'min' });
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionStartOfLineBackwordsCommand extends EasyMotionLineMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'k' });
|
||||
|
||||
constructor() {
|
||||
super({ searchOptions: 'max' });
|
||||
}
|
||||
}
|
||||
|
||||
// easymotion-bd-jk
|
||||
|
||||
@RegisterAction
|
||||
class EasyMotionStartOfLineBidirectionalCommand extends EasyMotionLineMoveCommandBase {
|
||||
keys = buildTriggerKeys({ key: 'bdjk', leaderCount: 3 });
|
||||
}
|
89
src/actions/plugins/easymotion/types.ts
Normal file
89
src/actions/plugins/easymotion/types.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Position } from 'vscode';
|
||||
import { Mode } from '../../../mode/mode';
|
||||
import type { VimState } from '../../../state/vimState';
|
||||
|
||||
export type LabelPosition = 'after' | 'before';
|
||||
export type JumpToAnywhere = true | false;
|
||||
|
||||
export interface EasyMotionMoveOptionsBase {
|
||||
searchOptions?: 'min' | 'max';
|
||||
}
|
||||
|
||||
export interface EasyMotionCharMoveOpions extends EasyMotionMoveOptionsBase {
|
||||
charCount: number;
|
||||
labelPosition?: LabelPosition;
|
||||
}
|
||||
|
||||
export interface EasyMotionWordMoveOpions extends EasyMotionMoveOptionsBase {
|
||||
labelPosition?: LabelPosition;
|
||||
jumpToAnywhere?: JumpToAnywhere;
|
||||
}
|
||||
|
||||
export interface Marker {
|
||||
name: string;
|
||||
position: Position;
|
||||
}
|
||||
|
||||
export class Match {
|
||||
public position: Position;
|
||||
public readonly text: string;
|
||||
public readonly index: number;
|
||||
|
||||
constructor(position: Position, text: string, index: number) {
|
||||
this.position = position;
|
||||
this.text = text;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public toRange(): vscode.Range {
|
||||
return new vscode.Range(this.position, this.position.translate(0, this.text.length));
|
||||
}
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
/**
|
||||
* The minimum bound of the search
|
||||
*/
|
||||
min?: Position;
|
||||
|
||||
/**
|
||||
* The maximum bound of the search
|
||||
*/
|
||||
max?: Position;
|
||||
}
|
||||
|
||||
export interface EasyMotionSearchAction {
|
||||
searchString: string;
|
||||
|
||||
/**
|
||||
* True if it should go to Easymotion mode
|
||||
*/
|
||||
shouldFire(): boolean;
|
||||
|
||||
/**
|
||||
* Command to execute when it should fire
|
||||
*/
|
||||
fire(position: Position, vimState: VimState): Promise<void>;
|
||||
getMatches(position: Position, vimState: VimState): Match[];
|
||||
readonly searchCharCount: number;
|
||||
}
|
||||
|
||||
export interface IEasyMotion {
|
||||
accumulation: string;
|
||||
previousMode: Mode;
|
||||
markers: Marker[];
|
||||
searchAction: EasyMotionSearchAction;
|
||||
|
||||
addMarker(marker: Marker): void;
|
||||
findMarkers(nail: string, onlyVisible: boolean): Marker[];
|
||||
sortedSearch(
|
||||
document: vscode.TextDocument,
|
||||
position: Position,
|
||||
search?: string | RegExp,
|
||||
options?: SearchOptions,
|
||||
): Match[];
|
||||
updateDecorations(editor: vscode.TextEditor): void;
|
||||
clearMarkers(): void;
|
||||
clearDecorations(editor: vscode.TextEditor): void;
|
||||
}
|
92
src/actions/plugins/imswitcher.ts
Normal file
92
src/actions/plugins/imswitcher.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { Logger } from '../../util/logger';
|
||||
import { Mode } from '../../mode/mode';
|
||||
import { configuration } from '../../configuration/configuration';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
/**
|
||||
* This function executes a shell command and returns the standard output as a string.
|
||||
*/
|
||||
function executeShell(cmd: string): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* InputMethodSwitcher changes input method when mode changed
|
||||
*/
|
||||
export class InputMethodSwitcher {
|
||||
private execute: (cmd: string) => Promise<string>;
|
||||
private savedIMKey = '';
|
||||
|
||||
constructor(execute: (cmd: string) => Promise<string> = executeShell) {
|
||||
this.execute = execute;
|
||||
}
|
||||
|
||||
public async switchInputMethod(prevMode: Mode, newMode: Mode) {
|
||||
if (configuration.autoSwitchInputMethod.enable !== true) {
|
||||
return;
|
||||
}
|
||||
// when you exit from insert-like mode, save origin input method and set it to default
|
||||
const isPrevModeInsertLike = this.isInsertLikeMode(prevMode);
|
||||
const isNewModeInsertLike = this.isInsertLikeMode(newMode);
|
||||
if (isPrevModeInsertLike !== isNewModeInsertLike) {
|
||||
if (isNewModeInsertLike) {
|
||||
await this.resumeIM();
|
||||
} else {
|
||||
await this.switchToDefaultIM();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// save origin input method and set input method to default
|
||||
private async switchToDefaultIM() {
|
||||
const obtainIMCmd = configuration.autoSwitchInputMethod.obtainIMCmd;
|
||||
try {
|
||||
const insertIMKey = await this.execute(obtainIMCmd);
|
||||
if (insertIMKey !== undefined) {
|
||||
this.savedIMKey = insertIMKey.trim();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error(`Error switching to default IM. err=${e}`);
|
||||
}
|
||||
|
||||
const defaultIMKey = configuration.autoSwitchInputMethod.defaultIM;
|
||||
if (defaultIMKey !== this.savedIMKey) {
|
||||
await this.switchToIM(defaultIMKey);
|
||||
}
|
||||
}
|
||||
|
||||
// resume origin inputmethod
|
||||
private async resumeIM() {
|
||||
if (this.savedIMKey !== configuration.autoSwitchInputMethod.defaultIM) {
|
||||
await this.switchToIM(this.savedIMKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async switchToIM(imKey: string) {
|
||||
let switchIMCmd = configuration.autoSwitchInputMethod.switchIMCmd;
|
||||
if (imKey !== '' && imKey !== undefined) {
|
||||
switchIMCmd = switchIMCmd.replace('{im}', imKey);
|
||||
try {
|
||||
await this.execute(switchIMCmd);
|
||||
} catch (e) {
|
||||
Logger.error(`Error switching to IM. err=${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isInsertLikeMode(mode: Mode): boolean {
|
||||
return [Mode.Insert, Mode.Replace, Mode.SurroundInputMode].includes(mode);
|
||||
}
|
||||
}
|
38
src/actions/plugins/pluginDefaultMappings.ts
Normal file
38
src/actions/plugins/pluginDefaultMappings.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { IConfiguration, IKeyRemapping } from '../../configuration/iconfiguration';
|
||||
|
||||
export class PluginDefaultMappings {
|
||||
// plugin authers may add entries here
|
||||
private static defaultMappings: Array<{
|
||||
mode: string;
|
||||
configSwitch: string;
|
||||
mapping: IKeyRemapping;
|
||||
}> = [
|
||||
// default maps for surround
|
||||
{
|
||||
mode: 'normalModeKeyBindingsNonRecursive',
|
||||
configSwitch: 'surround',
|
||||
mapping: { before: ['y', 's'], after: ['<plugys>'] },
|
||||
},
|
||||
{
|
||||
mode: 'normalModeKeyBindingsNonRecursive',
|
||||
configSwitch: 'surround',
|
||||
mapping: { before: ['y', 's', 's'], after: ['<plugys>', '<plugys>'] },
|
||||
},
|
||||
{
|
||||
mode: 'normalModeKeyBindingsNonRecursive',
|
||||
configSwitch: 'surround',
|
||||
mapping: { before: ['c', 's'], after: ['<plugcs>'] },
|
||||
},
|
||||
{
|
||||
mode: 'normalModeKeyBindingsNonRecursive',
|
||||
configSwitch: 'surround',
|
||||
mapping: { before: ['d', 's'], after: ['<plugds>'] },
|
||||
},
|
||||
];
|
||||
|
||||
public static getPluginDefaultMappings(mode: string, config: IConfiguration): IKeyRemapping[] {
|
||||
return this.defaultMappings
|
||||
.filter((m) => m.mode === mode && config[m.configSwitch])
|
||||
.map((m) => m.mapping);
|
||||
}
|
||||
}
|
71
src/actions/plugins/replaceWithRegister.ts
Normal file
71
src/actions/plugins/replaceWithRegister.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { configuration } from '../../configuration/configuration';
|
||||
import { Mode } from '../../mode/mode';
|
||||
import { Register, RegisterMode } from '../../register/register';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { BaseOperator } from '../operator';
|
||||
import { RegisterAction } from './../base';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { VimError, ErrorCode } from '../../error';
|
||||
import { Position, Range } from 'vscode';
|
||||
import { PositionDiff } from '../../common/motion/position';
|
||||
|
||||
@RegisterAction
|
||||
class ReplaceOperator extends BaseOperator {
|
||||
public keys = ['g', 'r'];
|
||||
public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine];
|
||||
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
return configuration.replaceWithRegister && super.doesActionApply(vimState, keysPressed);
|
||||
}
|
||||
|
||||
public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
return configuration.replaceWithRegister && super.doesActionApply(vimState, keysPressed);
|
||||
}
|
||||
|
||||
public async run(vimState: VimState, start: Position, end: Position): Promise<void> {
|
||||
const range =
|
||||
vimState.currentRegisterMode === RegisterMode.LineWise
|
||||
? new Range(start.getLineBegin(), end.getLineEndIncludingEOL())
|
||||
: new Range(start, end.getRight());
|
||||
|
||||
const register = await Register.get(vimState.recordedState.registerName, this.multicursorIndex);
|
||||
if (register === undefined) {
|
||||
StatusBar.displayError(
|
||||
vimState,
|
||||
VimError.fromCode(ErrorCode.NothingInRegister, vimState.recordedState.registerName),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const replaceWith = register.text as string;
|
||||
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'replaceText',
|
||||
range,
|
||||
text: replaceWith,
|
||||
diff: PositionDiff.exactPosition(getCursorPosition(vimState, range, replaceWith)),
|
||||
});
|
||||
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
}
|
||||
}
|
||||
|
||||
const getCursorPosition = (vimState: VimState, range: Range, replaceWith: string): Position => {
|
||||
const {
|
||||
recordedState: { actionKeys },
|
||||
} = vimState;
|
||||
const lines = replaceWith.split('\n');
|
||||
const wasRunAsLineAction = actionKeys.indexOf('r') === 0 && actionKeys.length === 1; // ie. grr
|
||||
const registerAndRangeAreSingleLines = lines.length === 1 && range.isSingleLine;
|
||||
const singleLineAction = registerAndRangeAreSingleLines && !wasRunAsLineAction;
|
||||
|
||||
return singleLineAction
|
||||
? cursorAtEndOfReplacement(range, replaceWith)
|
||||
: cursorAtFirstNonBlankCharOfLine(range.start.line, lines[0]);
|
||||
};
|
||||
|
||||
const cursorAtEndOfReplacement = (range: Range, replacement: string) =>
|
||||
new Position(range.start.line, Math.max(0, range.start.character + replacement.length - 1));
|
||||
|
||||
const cursorAtFirstNonBlankCharOfLine = (line: number, text: string) =>
|
||||
new Position(line, text.match(/\S/)?.index ?? 0);
|
138
src/actions/plugins/sneak.ts
Normal file
138
src/actions/plugins/sneak.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { configuration } from './../../configuration/configuration';
|
||||
import { RegisterAction } from './../base';
|
||||
import { BaseMovement, IMovement } from '../baseMotion';
|
||||
import { Position } from 'vscode';
|
||||
|
||||
@RegisterAction
|
||||
export class SneakForward extends BaseMovement {
|
||||
keys = [
|
||||
['s', '<character>', '<character>'],
|
||||
['z', '<character>', '<character>'],
|
||||
];
|
||||
override isJump = true;
|
||||
|
||||
public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
const startingLetter = vimState.recordedState.operator === undefined ? 's' : 'z';
|
||||
|
||||
return (
|
||||
configuration.sneak &&
|
||||
super.couldActionApply(vimState, keysPressed) &&
|
||||
keysPressed[0] === startingLetter
|
||||
);
|
||||
}
|
||||
|
||||
public override async execAction(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
): Promise<Position | IMovement> {
|
||||
if (!this.isRepeat) {
|
||||
vimState.lastSemicolonRepeatableMovement = new SneakForward(this.keysPressed, true);
|
||||
vimState.lastCommaRepeatableMovement = new SneakBackward(this.keysPressed, true);
|
||||
}
|
||||
|
||||
if (this.keysPressed[2] === '\n') {
|
||||
// Single key sneak
|
||||
this.keysPressed[2] = '';
|
||||
}
|
||||
|
||||
const searchString = this.keysPressed[1] + this.keysPressed[2];
|
||||
|
||||
const document = vimState.document;
|
||||
const lineCount = document.lineCount;
|
||||
for (let i = position.line; i < lineCount; ++i) {
|
||||
const lineText = document.lineAt(i).text;
|
||||
|
||||
// Start searching after the current character so we don't find the same match twice
|
||||
const fromIndex = i === position.line ? position.character + 1 : 0;
|
||||
|
||||
let matchIndex = -1;
|
||||
|
||||
const ignorecase =
|
||||
configuration.sneakUseIgnorecaseAndSmartcase &&
|
||||
configuration.ignorecase &&
|
||||
!(configuration.smartcase && /[A-Z]/.test(searchString));
|
||||
|
||||
// Check for matches
|
||||
if (ignorecase) {
|
||||
matchIndex = lineText
|
||||
.toLocaleLowerCase()
|
||||
.indexOf(searchString.toLocaleLowerCase(), fromIndex);
|
||||
} else {
|
||||
matchIndex = lineText.indexOf(searchString, fromIndex);
|
||||
}
|
||||
|
||||
if (matchIndex >= 0) {
|
||||
return new Position(i, matchIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class SneakBackward extends BaseMovement {
|
||||
keys = [
|
||||
['S', '<character>', '<character>'],
|
||||
['Z', '<character>', '<character>'],
|
||||
];
|
||||
override isJump = true;
|
||||
|
||||
public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
const startingLetter = vimState.recordedState.operator === undefined ? 'S' : 'Z';
|
||||
|
||||
return (
|
||||
configuration.sneak &&
|
||||
super.couldActionApply(vimState, keysPressed) &&
|
||||
keysPressed[0] === startingLetter
|
||||
);
|
||||
}
|
||||
|
||||
public override async execAction(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
): Promise<Position | IMovement> {
|
||||
if (!this.isRepeat) {
|
||||
vimState.lastSemicolonRepeatableMovement = new SneakBackward(this.keysPressed, true);
|
||||
vimState.lastCommaRepeatableMovement = new SneakForward(this.keysPressed, true);
|
||||
}
|
||||
|
||||
if (this.keysPressed[2] === '\n') {
|
||||
// Single key sneak
|
||||
this.keysPressed[2] = '';
|
||||
}
|
||||
|
||||
const searchString = this.keysPressed[1] + this.keysPressed[2];
|
||||
|
||||
const document = vimState.document;
|
||||
for (let i = position.line; i >= 0; --i) {
|
||||
const lineText = document.lineAt(i).text;
|
||||
|
||||
// Start searching before the current character so we don't find the same match twice
|
||||
const fromIndex = i === position.line ? position.character - 1 : +Infinity;
|
||||
|
||||
let matchIndex = -1;
|
||||
|
||||
const ignorecase =
|
||||
configuration.sneakUseIgnorecaseAndSmartcase &&
|
||||
configuration.ignorecase &&
|
||||
!(configuration.smartcase && /[A-Z]/.test(searchString));
|
||||
|
||||
// Check for matches
|
||||
if (ignorecase) {
|
||||
matchIndex = lineText
|
||||
.toLocaleLowerCase()
|
||||
.lastIndexOf(searchString.toLocaleLowerCase(), fromIndex);
|
||||
} else {
|
||||
matchIndex = lineText.lastIndexOf(searchString, fromIndex);
|
||||
}
|
||||
|
||||
if (matchIndex >= 0) {
|
||||
return new Position(i, matchIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
}
|
683
src/actions/plugins/surround.ts
Normal file
683
src/actions/plugins/surround.ts
Normal file
@ -0,0 +1,683 @@
|
||||
import { Position, Range, window } from 'vscode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import {
|
||||
SelectABigWord,
|
||||
SelectInnerWord,
|
||||
SelectWord,
|
||||
TextObject,
|
||||
} from '../../textobject/textobject';
|
||||
import { isIMovement } from '../baseMotion';
|
||||
import {
|
||||
MoveAroundBacktick,
|
||||
MoveAroundCaret,
|
||||
MoveAroundCurlyBrace,
|
||||
MoveAroundDoubleQuotes,
|
||||
MoveAroundParentheses,
|
||||
MoveAroundSingleQuotes,
|
||||
MoveAroundSquareBracket,
|
||||
MoveAroundTag,
|
||||
MoveFullWordBegin,
|
||||
MoveInsideCharacter,
|
||||
MoveInsideTag,
|
||||
MoveQuoteMatch,
|
||||
MoveWordBegin,
|
||||
} from '../motion';
|
||||
import { PositionDiff, sorted } from './../../common/motion/position';
|
||||
import { configuration } from './../../configuration/configuration';
|
||||
import { Mode } from './../../mode/mode';
|
||||
import { BaseCommand, RegisterAction } from './../base';
|
||||
import { BaseOperator } from './../operator';
|
||||
|
||||
type SurroundEdge = {
|
||||
leftEdge: Range;
|
||||
rightEdge: Range;
|
||||
/** we need to pass this with transformations */
|
||||
cursorIndex: number;
|
||||
/** to support changing a tag, cstt */
|
||||
leftTagName?: Range;
|
||||
rightTagName?: Range;
|
||||
};
|
||||
|
||||
type TagReplacement = {
|
||||
tag: string;
|
||||
/** when changing tag to tag, do we keep attributes? default: yes */
|
||||
keepAttributes: boolean;
|
||||
};
|
||||
|
||||
export interface SurroundState {
|
||||
/** The operator paired with the surround action. "yank" is really "add", but it uses 'y' */
|
||||
operator: 'change' | 'delete' | 'yank';
|
||||
|
||||
/** target of surround op: X in csXy and dsX */
|
||||
target: string | undefined;
|
||||
|
||||
/** the added surrounding, like ",',(). t = tag */
|
||||
replacement: string;
|
||||
|
||||
/** name of tag */
|
||||
tag?: TagReplacement;
|
||||
|
||||
/** name of function */
|
||||
function?: string;
|
||||
|
||||
/** for visual line mode */
|
||||
addNewline?: boolean;
|
||||
|
||||
edges: SurroundEdge[];
|
||||
|
||||
/** The mode before surround was triggered */
|
||||
previousMode: Mode;
|
||||
}
|
||||
|
||||
abstract class SurroundOperator extends BaseOperator {
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
return configuration.surround && super.doesActionApply(vimState, keysPressed);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class YankSurroundOperator extends SurroundOperator {
|
||||
// needs: nnoremap ys <plugys>. we leave it to Remapper to figure out y vs ys.
|
||||
public keys = ['<plugys>'];
|
||||
public modes = [Mode.Normal];
|
||||
|
||||
public async run(vimState: VimState, start: Position, end: Position): Promise<void> {
|
||||
// reset surround state when run for first cursor
|
||||
if (!this.multicursorIndex) {
|
||||
vimState.surround = {
|
||||
operator: 'yank',
|
||||
target: undefined,
|
||||
replacement: '',
|
||||
edges: [],
|
||||
previousMode: vimState.currentMode,
|
||||
};
|
||||
}
|
||||
const getYankRanges = (): SurroundEdge => {
|
||||
// for special handling for w motion.
|
||||
// with "|surroundme ZONK" it will jump to Z, but we just want surroundme
|
||||
const endPlus1 = new Range(end.getRight(), end.getRight());
|
||||
const prevWordEnd = end.getRight().prevWordEnd(vimState.document);
|
||||
const endW = new Range(prevWordEnd.getRight(), prevWordEnd.getRight());
|
||||
const lastMotion =
|
||||
vimState.recordedState.actionsRun[vimState.recordedState.actionsRun.length - 1];
|
||||
const ranWwMotion =
|
||||
lastMotion instanceof MoveWordBegin ||
|
||||
lastMotion instanceof MoveFullWordBegin ||
|
||||
lastMotion instanceof SelectABigWord ||
|
||||
lastMotion instanceof SelectWord;
|
||||
const rightEdge = ranWwMotion ? endW : endPlus1;
|
||||
return {
|
||||
leftEdge: new Range(start, start),
|
||||
rightEdge,
|
||||
cursorIndex: multicursorIndex,
|
||||
};
|
||||
};
|
||||
// then collect ranges for all cursors
|
||||
const multicursorIndex = this.multicursorIndex ?? 0;
|
||||
vimState.surround!.edges.push(getYankRanges());
|
||||
vimState.cursorStartPosition = start;
|
||||
// when called from visual operator, use end for stop to keep visual selection
|
||||
vimState.cursorStopPosition = vimState.currentMode === Mode.Visual ? end : start;
|
||||
await vimState.setCurrentMode(Mode.SurroundInputMode);
|
||||
}
|
||||
|
||||
public override async runRepeat(
|
||||
vimState: VimState,
|
||||
position: Position,
|
||||
count: number,
|
||||
): Promise<void> {
|
||||
// we want to act on range: first non whitespace to last non whitespace
|
||||
await this.run(
|
||||
vimState,
|
||||
position.getLineBeginRespectingIndent(vimState.document),
|
||||
position
|
||||
.getDown(Math.max(0, count - 1))
|
||||
.getLineEnd()
|
||||
.prevWordEnd(vimState.document),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSurroundModeStartVisual extends SurroundOperator {
|
||||
modes = [Mode.Visual];
|
||||
keys = ['S'];
|
||||
|
||||
public async run(vimState: VimState, start: Position, end: Position): Promise<void> {
|
||||
[start, end] = sorted(start, end);
|
||||
await new YankSurroundOperator(this.multicursorIndex).run(vimState, start, end);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSurroundModeStartVisualLine extends SurroundOperator {
|
||||
modes = [Mode.VisualLine];
|
||||
keys = ['S'];
|
||||
|
||||
public async run(vimState: VimState, start: Position, end: Position): Promise<void> {
|
||||
[start, end] = sorted(start.getLineBegin(), end.getLineEnd());
|
||||
|
||||
// reset surround state when run for first cursor
|
||||
if (!this.multicursorIndex) {
|
||||
vimState.surround = {
|
||||
target: undefined,
|
||||
operator: 'yank',
|
||||
replacement: '',
|
||||
addNewline: true,
|
||||
edges: [],
|
||||
previousMode: vimState.currentMode,
|
||||
};
|
||||
}
|
||||
|
||||
// collect ranges for all cursors
|
||||
vimState.surround?.edges.push({
|
||||
leftEdge: new Range(start, start),
|
||||
rightEdge: new Range(end, end),
|
||||
cursorIndex: this.multicursorIndex ?? 0,
|
||||
});
|
||||
|
||||
vimState.cursorStartPosition = start;
|
||||
vimState.cursorStopPosition = end;
|
||||
await vimState.setCurrentMode(Mode.SurroundInputMode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CommandSurround extends BaseCommand {
|
||||
modes = [Mode.Normal];
|
||||
override createsUndoPoint = true;
|
||||
override runsOnceForEveryCursor() {
|
||||
return true;
|
||||
}
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
const target = keysPressed[keysPressed.length - 1];
|
||||
return (
|
||||
configuration.surround &&
|
||||
super.doesActionApply(vimState, keysPressed) &&
|
||||
SurroundHelper.edgePairings[target] !== undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSurroundDeleteSurround extends CommandSurround {
|
||||
keys = ['<plugds>', '<any>'];
|
||||
keysHasCnt = false;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const target = this.keysPressed[this.keysPressed.length - 1];
|
||||
// for derived class, support ds2X
|
||||
if (this.keysHasCnt) {
|
||||
const cntKey = this.keysPressed[this.keysPressed.length - 2];
|
||||
// eslint-disable-next-line radix
|
||||
vimState.recordedState.count = parseInt(cntKey, undefined);
|
||||
}
|
||||
|
||||
// for this operator, we set surround state and execute for each cursor one at a time
|
||||
vimState.surround = {
|
||||
operator: 'delete',
|
||||
target,
|
||||
replacement: '',
|
||||
edges: [],
|
||||
previousMode: Mode.Normal,
|
||||
};
|
||||
|
||||
// we need surround state initiated for this call
|
||||
const replaceRanges = await SurroundHelper.getReplaceRanges(
|
||||
vimState,
|
||||
position,
|
||||
this.multicursorIndex ?? 0,
|
||||
);
|
||||
|
||||
if (replaceRanges) {
|
||||
vimState.surround.edges = [replaceRanges];
|
||||
await SurroundHelper.ExecuteSurround(vimState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSurroundDeleteSurroundCnt extends CommandSurroundDeleteSurround {
|
||||
// supports cnt up to 9, should be enough
|
||||
override keys = ['<plugds>', '<number>', '<any>'];
|
||||
override keysHasCnt = true;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSurroundChangeSurround extends CommandSurround {
|
||||
keys = ['<plugcs>', '<any>'];
|
||||
override isCompleteAction = false;
|
||||
keysHasCnt = false;
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const target = this.keysPressed[this.keysPressed.length - 1];
|
||||
// for derived class, support ds2X
|
||||
if (this.keysHasCnt) {
|
||||
const cntKey = this.keysPressed[this.keysPressed.length - 2];
|
||||
// eslint-disable-next-line radix
|
||||
vimState.recordedState.count = parseInt(cntKey, undefined);
|
||||
}
|
||||
|
||||
// reset surround state when run for first cursor
|
||||
if (!this.multicursorIndex) {
|
||||
vimState.surround = {
|
||||
operator: 'change',
|
||||
target,
|
||||
replacement: '',
|
||||
edges: [],
|
||||
previousMode: Mode.Normal,
|
||||
};
|
||||
}
|
||||
|
||||
// we need state surround initiated for this call
|
||||
const replaceRanges = await SurroundHelper.getReplaceRanges(
|
||||
vimState,
|
||||
position,
|
||||
this.multicursorIndex ?? 0,
|
||||
);
|
||||
|
||||
// collect ranges for all cursors
|
||||
if (replaceRanges) {
|
||||
vimState.surround!.edges.push(replaceRanges);
|
||||
}
|
||||
await vimState.setCurrentMode(Mode.SurroundInputMode);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSurroundChangeSurroundCnt extends CommandSurroundChangeSurround {
|
||||
// supports cnt up to 9, should be enough
|
||||
override keys = ['<plugcs>', '<number>', '<any>'];
|
||||
override keysHasCnt = true;
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class CommandSurroundAddSurrounding extends BaseCommand {
|
||||
modes = [Mode.SurroundInputMode];
|
||||
// add surrounding / read X when: ys + motion + X. or csYX
|
||||
keys = ['<any>'];
|
||||
override isCompleteAction = true;
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
const replacement = keysPressed[keysPressed.length - 1];
|
||||
return (
|
||||
configuration.surround &&
|
||||
super.doesActionApply(vimState, keysPressed) &&
|
||||
replacement !== 't' && // do not run this for surrounding with a tag
|
||||
replacement !== '<' &&
|
||||
replacement !== 'f' && // or for surrounding with a function
|
||||
replacement !== 'F' &&
|
||||
replacement !== '<C-f>'
|
||||
);
|
||||
}
|
||||
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
const replacement = this.keysPressed[this.keysPressed.length - 1];
|
||||
|
||||
if (!vimState.surround || !SurroundHelper.edgePairings[replacement]) {
|
||||
// cant surround, abort.
|
||||
// this typically handles, when last keypress was wrong and not a valid surrounding
|
||||
vimState.surround = undefined;
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
return;
|
||||
}
|
||||
|
||||
vimState.surround.replacement = replacement;
|
||||
|
||||
await SurroundHelper.ExecuteSurround(vimState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class CommandSurroundAddSurroundingTag extends BaseCommand {
|
||||
modes = [Mode.SurroundInputMode];
|
||||
// add surrounding / read X when: ys + motion + X
|
||||
keys = [['<'], ['t']];
|
||||
override isCompleteAction = true;
|
||||
recordedTag = ''; // to save for repeat
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
if (!vimState.surround) {
|
||||
return;
|
||||
}
|
||||
|
||||
vimState.surround.replacement = 't';
|
||||
const tagInput =
|
||||
vimState.isRunningDotCommand || vimState.isReplayingMacro
|
||||
? this.recordedTag
|
||||
: await this.readTag();
|
||||
|
||||
if (!tagInput) {
|
||||
vimState.surround = undefined;
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
return;
|
||||
}
|
||||
|
||||
// record tag for repeat. this works because recordedState will store the actual objects
|
||||
this.recordedTag = tagInput;
|
||||
|
||||
// local helper
|
||||
const checkReplaceAttributes = (tag: string) => {
|
||||
return tag.substring(tag.length - 1) === '>'
|
||||
? { tag: tag.substring(0, tag.length - 1), keepAttributes: false }
|
||||
: { tag, keepAttributes: true };
|
||||
};
|
||||
|
||||
// check as special case (set by >) if we want to replace the attributes on tag or keep them (default)
|
||||
vimState.surround.tag = checkReplaceAttributes(tagInput);
|
||||
|
||||
// finally, we can exec surround
|
||||
await SurroundHelper.ExecuteSurround(vimState);
|
||||
}
|
||||
|
||||
private async readTag(): Promise<string | undefined> {
|
||||
return window.showInputBox({
|
||||
prompt: 'Enter tag',
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
export class CommandSurroundAddSurroundingFunction extends BaseCommand {
|
||||
modes = [Mode.SurroundInputMode];
|
||||
// add surrounding / read X when: ys + motion + X
|
||||
keys = [['f'], ['F'], ['<C-f>']];
|
||||
override isCompleteAction = true;
|
||||
recordedFunction = ''; // to save for repeat
|
||||
override runsOnceForEveryCursor() {
|
||||
return false;
|
||||
}
|
||||
public override async exec(position: Position, vimState: VimState): Promise<void> {
|
||||
if (!vimState.surround) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reuse the spacing logic from the parentheses
|
||||
// for the right side of the replacement
|
||||
vimState.surround.replacement =
|
||||
this.keysPressed[this.keysPressed.length - 1] === 'F' ? '(' : ')';
|
||||
|
||||
const functionInput =
|
||||
vimState.isRunningDotCommand || vimState.isReplayingMacro
|
||||
? this.recordedFunction
|
||||
: await this.readFunction();
|
||||
|
||||
if (!functionInput) {
|
||||
vimState.surround = undefined;
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
return;
|
||||
}
|
||||
|
||||
// record function for repeat.
|
||||
this.recordedFunction = functionInput;
|
||||
|
||||
// format the left side of the replacement based on the key pressed
|
||||
vimState.surround.function = this.formatFunction(functionInput);
|
||||
|
||||
await SurroundHelper.ExecuteSurround(vimState);
|
||||
}
|
||||
|
||||
private async readFunction(): Promise<string | undefined> {
|
||||
return window.showInputBox({
|
||||
prompt: 'Enter function',
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
}
|
||||
|
||||
private formatFunction(fn: string): string {
|
||||
switch (this.keysPressed[this.keysPressed.length - 1]) {
|
||||
case 'f':
|
||||
return fn + '(';
|
||||
case 'F':
|
||||
return fn + '( ';
|
||||
case '<C-f>':
|
||||
default:
|
||||
return '(' + fn + ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// following are static internal helper functions
|
||||
// top level helper is ExecuteSurround, which is called from exec and does the actual text transformations
|
||||
class SurroundHelper {
|
||||
/** a map which holds for each target key: inserted text + implementation helper */
|
||||
static edgePairings: {
|
||||
[key: string]: {
|
||||
left: string;
|
||||
right: string;
|
||||
/** do we consume space on the edges? "(" vs ")" */
|
||||
removeSpace: boolean;
|
||||
movement: () => MoveInsideCharacter | MoveQuoteMatch | MoveAroundTag | TextObject;
|
||||
/** typically to extend an inner word. with *foo*, from "foo" to "*foo*" */
|
||||
extraChars?: number;
|
||||
};
|
||||
} = {
|
||||
// helpful linter is helpful :-D
|
||||
'(': {
|
||||
left: '( ',
|
||||
right: ' )',
|
||||
removeSpace: true,
|
||||
movement: () => new MoveAroundParentheses(),
|
||||
},
|
||||
')': { left: '(', right: ')', removeSpace: false, movement: () => new MoveAroundParentheses() },
|
||||
'[': {
|
||||
left: '[ ',
|
||||
right: ' ]',
|
||||
removeSpace: true,
|
||||
movement: () => new MoveAroundSquareBracket(),
|
||||
},
|
||||
']': {
|
||||
left: '[',
|
||||
right: ']',
|
||||
removeSpace: false,
|
||||
movement: () => new MoveAroundSquareBracket(),
|
||||
},
|
||||
'{': { left: '{ ', right: ' }', removeSpace: true, movement: () => new MoveAroundCurlyBrace() },
|
||||
'}': { left: '{', right: '}', removeSpace: false, movement: () => new MoveAroundCurlyBrace() },
|
||||
'>': { left: '<', right: '>', removeSpace: false, movement: () => new MoveAroundCaret() },
|
||||
'"': {
|
||||
left: '"',
|
||||
right: '"',
|
||||
removeSpace: false,
|
||||
movement: () => new MoveAroundDoubleQuotes(false),
|
||||
},
|
||||
"'": {
|
||||
left: "'",
|
||||
right: "'",
|
||||
removeSpace: false,
|
||||
movement: () => new MoveAroundSingleQuotes(false),
|
||||
},
|
||||
'`': {
|
||||
left: '`',
|
||||
right: '`',
|
||||
removeSpace: false,
|
||||
movement: () => new MoveAroundBacktick(false),
|
||||
},
|
||||
'<': { left: '', right: '', removeSpace: false, movement: () => new MoveAroundTag() },
|
||||
'*': {
|
||||
left: '*',
|
||||
right: '*',
|
||||
removeSpace: false,
|
||||
movement: () => new SelectInnerWord(),
|
||||
extraChars: 1,
|
||||
},
|
||||
// aliases
|
||||
b: { left: '(', right: ')', removeSpace: false, movement: () => new MoveAroundParentheses() },
|
||||
r: { left: '[', right: ']', removeSpace: false, movement: () => new MoveAroundSquareBracket() },
|
||||
B: { left: '{', right: '}', removeSpace: false, movement: () => new MoveAroundCurlyBrace() },
|
||||
a: { left: '<', right: '>', removeSpace: false, movement: () => new MoveAroundCaret() },
|
||||
t: { left: '', right: '', removeSpace: false, movement: () => new MoveAroundTag() },
|
||||
_: { left: '_', right: '_', removeSpace: false, movement: () => new SelectInnerWord() },
|
||||
};
|
||||
|
||||
/** returns two ranges (for left and right replacement) for our surround target (X in dsX, csXy) relative to position */
|
||||
public static async getReplaceRanges(
|
||||
vimState: VimState,
|
||||
position: Position,
|
||||
multicursorIndex: number,
|
||||
): Promise<SurroundEdge | undefined> {
|
||||
/* so this method is a bit of a dumpster for edge cases and ugly details
|
||||
the main idea is this:
|
||||
1. from position, we execute a textobject movement to get the total range of our surround target
|
||||
2. from there, we derive two ranges (left and right), where to apply delete/change
|
||||
3. that our result to return
|
||||
*/
|
||||
|
||||
// input verification
|
||||
if (!vimState.surround || !vimState.surround.target) {
|
||||
return undefined;
|
||||
}
|
||||
const target = this.edgePairings[vimState.surround.target];
|
||||
if (!target) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// we want start, end of executing movement for surround target count times from position
|
||||
const { removeSpace, movement } = target;
|
||||
vimState.cursorStartPosition = position; // some textobj (MoveInsideCharacter) expect this
|
||||
const count = vimState.recordedState.count || 1;
|
||||
const targetMovement = await movement().execActionWithCount(position, vimState, count);
|
||||
if (!isIMovement(targetMovement) || !!targetMovement.failed) {
|
||||
// we want as result an IMovement, that did not fail.
|
||||
return undefined;
|
||||
}
|
||||
let rangeStart = targetMovement.start;
|
||||
let rangeEnd = targetMovement.stop;
|
||||
|
||||
// some local helpers
|
||||
const getAdjustedRanges = (): SurroundEdge => {
|
||||
if (movement() instanceof MoveInsideCharacter) {
|
||||
// for parens, brackets, curly ... we have to adjust the right range
|
||||
// there seems to be inconsistency between MoveInsideCharacter and MoveQuoteMatch
|
||||
rangeEnd = rangeEnd.getLeft();
|
||||
}
|
||||
if (target.extraChars) {
|
||||
rangeStart = rangeStart.getLeft(target.extraChars);
|
||||
rangeEnd = rangeEnd.getRight(target.extraChars);
|
||||
}
|
||||
// now start and end are on ()
|
||||
// next, check if there is space to remove (foo) vs ( bar )
|
||||
const delSpace = checkRemoveSpace(); // 0 or 1
|
||||
|
||||
return {
|
||||
leftEdge: new Range(rangeStart, rangeStart.getRight(1 + delSpace)),
|
||||
rightEdge: new Range(rangeEnd.getLeft(delSpace), rangeEnd.getRight()),
|
||||
cursorIndex: multicursorIndex,
|
||||
};
|
||||
};
|
||||
const checkRemoveSpace = (): number => {
|
||||
// capiche?
|
||||
const leftSpace = vimState.editor.document.getText(
|
||||
new Range(rangeStart.getRight(), rangeStart.getRight(2)),
|
||||
);
|
||||
const rightSpace = vimState.editor.document.getText(new Range(rangeEnd.getLeft(), rangeEnd));
|
||||
return removeSpace && leftSpace === ' ' && rightSpace === ' ' ? 1 : 0;
|
||||
};
|
||||
const getAdjustedRangesForTag = async (): Promise<SurroundEdge | undefined> => {
|
||||
// we are on start of opening tag and end of closing tag
|
||||
// return ranges from there to the other side
|
||||
// start -> <foo>bar</foo> <-- stop
|
||||
const openTagNameStart = rangeStart.getRight();
|
||||
const openTagNameEnd = openTagNameStart
|
||||
.nextWordEnd(vimState.document, { inclusive: true })
|
||||
.getRight();
|
||||
const closeTagNameStart = rangeEnd
|
||||
.getLeft(2)
|
||||
.prevWordStart(vimState.document, { inclusive: true });
|
||||
const closeTagNameEnd = rangeEnd.getLeft();
|
||||
vimState.cursorStartPosition = position; // some textobj (MoveInsideCharacter) expect this
|
||||
vimState.cursorStopPosition = position;
|
||||
const innerTag =
|
||||
count === 1
|
||||
? await new MoveInsideTag().execActionWithCount(position, vimState, 1)
|
||||
: await new MoveAroundTag().execActionWithCount(position, vimState, count - 1);
|
||||
if (!isIMovement(innerTag) || !!innerTag.failed) {
|
||||
return undefined;
|
||||
} else {
|
||||
return {
|
||||
leftEdge: new Range(rangeStart, innerTag.start),
|
||||
// maybe there is a small bug with cstt for multicursor, 2nd+ cursors
|
||||
rightEdge: new Range(innerTag.stop, rangeEnd),
|
||||
leftTagName: new Range(openTagNameStart, openTagNameEnd),
|
||||
rightTagName: new Range(closeTagNameStart, closeTagNameEnd),
|
||||
cursorIndex: multicursorIndex,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// good to go, now we can calculate our ranges based on rangeStart and rangeEnd
|
||||
return vimState.surround.target === 't' ? getAdjustedRangesForTag() : getAdjustedRanges();
|
||||
}
|
||||
|
||||
/** executes our prepared surround changes */
|
||||
public static async ExecuteSurround(vimState: VimState): Promise<void> {
|
||||
const surroundState = vimState.surround;
|
||||
if (!surroundState || !surroundState.edges) {
|
||||
return;
|
||||
}
|
||||
|
||||
const replacement = this.edgePairings[surroundState.replacement];
|
||||
// undefined allowed only for delete operator
|
||||
if (!replacement && surroundState.operator !== 'delete') {
|
||||
throw new Error('replacement missing in pairs');
|
||||
}
|
||||
// handle special case: cstt, replace only tag name
|
||||
if (surroundState.target === 't' && surroundState.tag && surroundState.tag.keepAttributes) {
|
||||
for (const { leftTagName, rightTagName } of surroundState.edges) {
|
||||
if (!surroundState.tag || !leftTagName || !rightTagName) {
|
||||
// throw ?
|
||||
continue;
|
||||
}
|
||||
vimState.recordedState.transformer.replace(leftTagName, surroundState.tag.tag);
|
||||
vimState.recordedState.transformer.replace(rightTagName, surroundState.tag.tag);
|
||||
}
|
||||
}
|
||||
// all other cases: ys, ds, cs
|
||||
else {
|
||||
const optNewline = surroundState.addNewline ? '\n' : '';
|
||||
const leftFixed =
|
||||
surroundState.operator === 'delete'
|
||||
? ''
|
||||
: surroundState.tag
|
||||
? '<' + surroundState.tag.tag + '>' + optNewline
|
||||
: surroundState.function
|
||||
? surroundState.function + optNewline
|
||||
: replacement.left + optNewline;
|
||||
|
||||
const rightFixed =
|
||||
surroundState.operator === 'delete'
|
||||
? ''
|
||||
: surroundState.tag
|
||||
? optNewline + '</' + surroundState.tag.tag + '>'
|
||||
: optNewline + replacement.right;
|
||||
|
||||
for (const { leftEdge, rightEdge, cursorIndex } of surroundState.edges) {
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'replaceText',
|
||||
text: leftFixed,
|
||||
range: leftEdge,
|
||||
cursorIndex,
|
||||
// keep cursor on left edge / start. todo: not completly correct vor visual S
|
||||
diff:
|
||||
surroundState.operator === 'yank'
|
||||
? PositionDiff.offset({ character: -leftFixed.length })
|
||||
: undefined,
|
||||
});
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'replaceText',
|
||||
text: rightFixed,
|
||||
range: rightEdge,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// finish / cleanup. sql-koala was here :D
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
}
|
||||
}
|
129
src/actions/plugins/targets/lastNextObjectHelper.ts
Normal file
129
src/actions/plugins/targets/lastNextObjectHelper.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import { Position } from 'vscode';
|
||||
import { isVisualMode } from '../../../mode/mode';
|
||||
import { VimState } from '../../../state/vimState';
|
||||
import { Logger } from '../../../util/logger';
|
||||
import { BaseMovement, failedMovement, IMovement } from '../../baseMotion';
|
||||
import { MoveInsideCharacter } from '../../motion';
|
||||
import { searchPosition } from './searchUtils';
|
||||
import { bracketObjectsEnabled } from './targetsConfig';
|
||||
|
||||
/*
|
||||
* This function creates a last/next movement based on an existing one.
|
||||
* It works by searching for a next/last character, and then applying the given action in its position.
|
||||
* For examples of how to use it, see src/actions/plugins/targets/lastNextObjects.ts.
|
||||
*/
|
||||
function LastNextObject<T extends MoveInsideCharacter>(type: new () => T, which: 'l' | 'n') {
|
||||
abstract class NextHandlerClass extends BaseMovement {
|
||||
public override readonly keys: readonly string[] | readonly string[][];
|
||||
override isJump = true;
|
||||
|
||||
// actual action (e.g. `i(` )
|
||||
private readonly actual: T;
|
||||
readonly secondKey: 'l' | 'n' = which;
|
||||
// character to search forward/backward for next/last (e.g. `(` for next parenthesis)
|
||||
abstract readonly charToFind: string;
|
||||
// this is just to make sure we won't register anything that we can't handle. see constructor.
|
||||
readonly valid: boolean;
|
||||
|
||||
public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
|
||||
return this.valid && bracketObjectsEnabled() && super.doesActionApply(vimState, keysPressed);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.actual = new type();
|
||||
|
||||
const secondKey = this.secondKey;
|
||||
const withWhichKey = (keys: string[]): string[] | undefined => {
|
||||
if (keys.length === 2) {
|
||||
return [keys[0], secondKey, keys[1]];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// we want fail without throwing an exception, but with log, to not break the Vim
|
||||
const errMsg = `failed to register ${which === 'l' ? 'last' : 'next'} for ${type.name}`;
|
||||
// failed, but it should never happen
|
||||
if (this.actual.keys.length < 1) {
|
||||
this.valid = false;
|
||||
this.keys = [];
|
||||
Logger.error(errMsg);
|
||||
return;
|
||||
}
|
||||
if (typeof this.actual.keys[0] === 'string') {
|
||||
const keys = withWhichKey(this.actual.keys as string[]);
|
||||
// failed
|
||||
if (keys === undefined) {
|
||||
this.valid = false;
|
||||
this.keys = [];
|
||||
Logger.error(errMsg);
|
||||
return;
|
||||
} else {
|
||||
this.keys = keys;
|
||||
}
|
||||
} else {
|
||||
const keys = this.actual.keys.map((k) => withWhichKey(k as string[]));
|
||||
// failed
|
||||
if (!keys.every((p) => p !== undefined)) {
|
||||
this.valid = false;
|
||||
this.keys = [];
|
||||
Logger.error(errMsg);
|
||||
return;
|
||||
} else {
|
||||
this.keys = keys as string[][];
|
||||
}
|
||||
}
|
||||
this.valid = true;
|
||||
}
|
||||
|
||||
public override async execAction(
|
||||
position: Position,
|
||||
vimState: VimState,
|
||||
firstIteration: boolean,
|
||||
lastIteration: boolean,
|
||||
): Promise<IMovement> {
|
||||
const maybePosition = searchPosition(this.charToFind, vimState.document, position, {
|
||||
direction: which === 'l' ? '<' : '>',
|
||||
includeCursor: false,
|
||||
throughLineBreaks: true,
|
||||
});
|
||||
if (maybePosition === undefined) {
|
||||
return failedMovement(vimState);
|
||||
}
|
||||
vimState.cursorStartPosition = maybePosition;
|
||||
vimState.cursorStopPosition = maybePosition;
|
||||
const movement = await this.actual.execAction(
|
||||
maybePosition,
|
||||
vimState,
|
||||
firstIteration,
|
||||
lastIteration,
|
||||
);
|
||||
if (movement.failed) {
|
||||
return movement;
|
||||
}
|
||||
const { start, stop } = movement;
|
||||
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.subtract(position);
|
||||
} else {
|
||||
vimState.recordedState.operatorPositionDiff = start.subtract(position);
|
||||
}
|
||||
}
|
||||
|
||||
vimState.cursorStartPosition = start;
|
||||
vimState.cursorStopPosition = stop;
|
||||
return movement;
|
||||
}
|
||||
}
|
||||
return NextHandlerClass;
|
||||
}
|
||||
|
||||
export function LastObject<T extends MoveInsideCharacter>(type: new () => T) {
|
||||
return LastNextObject(type, 'l');
|
||||
}
|
||||
export function NextObject<T extends MoveInsideCharacter>(type: new () => T) {
|
||||
return LastNextObject(type, 'n');
|
||||
}
|
92
src/actions/plugins/targets/lastNextObjects.ts
Normal file
92
src/actions/plugins/targets/lastNextObjects.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { RegisterAction } from '../../base';
|
||||
import {
|
||||
MoveAroundCaret,
|
||||
MoveAroundCurlyBrace,
|
||||
MoveAroundParentheses,
|
||||
MoveAroundSquareBracket,
|
||||
MoveInsideCaret,
|
||||
MoveInsideCurlyBrace,
|
||||
MoveInsideParentheses,
|
||||
MoveInsideSquareBracket,
|
||||
} from '../../motion';
|
||||
import { LastObject, NextObject } from './lastNextObjectHelper';
|
||||
|
||||
@RegisterAction
|
||||
class MoveInsideNextParentheses extends NextObject(MoveInsideParentheses) {
|
||||
override readonly charToFind: string = '(';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveInsideLastParentheses extends LastObject(MoveInsideParentheses) {
|
||||
override readonly charToFind: string = ')';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveAroundNextParentheses extends NextObject(MoveAroundParentheses) {
|
||||
override readonly charToFind: string = '(';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveAroundLastParentheses extends LastObject(MoveAroundParentheses) {
|
||||
override readonly charToFind: string = ')';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveInsideNextCurlyBrace extends NextObject(MoveInsideCurlyBrace) {
|
||||
override readonly charToFind: string = '{';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveInsideLastCurlyBrace extends LastObject(MoveInsideCurlyBrace) {
|
||||
override readonly charToFind: string = '}';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveAroundNextCurlyBrace extends NextObject(MoveAroundCurlyBrace) {
|
||||
override readonly charToFind: string = '{';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveAroundLastCurlyBrace extends LastObject(MoveAroundCurlyBrace) {
|
||||
override readonly charToFind: string = '}';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveInsideNextSquareBracket extends NextObject(MoveInsideSquareBracket) {
|
||||
override readonly charToFind: string = '[';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveInsideLastSquareBracket extends LastObject(MoveInsideSquareBracket) {
|
||||
override readonly charToFind: string = ']';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveAroundNextSquareBracket extends NextObject(MoveAroundSquareBracket) {
|
||||
override readonly charToFind: string = '[';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveAroundLastSquareBracket extends LastObject(MoveAroundSquareBracket) {
|
||||
override readonly charToFind: string = ']';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveInsideNextCaret extends NextObject(MoveInsideCaret) {
|
||||
override readonly charToFind: string = '<';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveInsideLastCaret extends LastObject(MoveInsideCaret) {
|
||||
override readonly charToFind: string = '>';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveAroundNextCaret extends NextObject(MoveAroundCaret) {
|
||||
override readonly charToFind: string = '<';
|
||||
}
|
||||
|
||||
@RegisterAction
|
||||
class MoveAroundLastCaret extends LastObject(MoveAroundCaret) {
|
||||
override readonly charToFind: string = '>';
|
||||
}
|
115
src/actions/plugins/targets/searchUtils.ts
Normal file
115
src/actions/plugins/targets/searchUtils.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { Position, TextDocument } from 'vscode';
|
||||
|
||||
export interface SearchFlags {
|
||||
direction?: '<' | '>';
|
||||
includeCursor?: boolean;
|
||||
throughLineBreaks?: boolean;
|
||||
}
|
||||
|
||||
function searchForward(
|
||||
str: string,
|
||||
document: TextDocument,
|
||||
start: Position,
|
||||
flags: {
|
||||
throughLineBreaks?: boolean;
|
||||
} = {
|
||||
throughLineBreaks: false,
|
||||
},
|
||||
): Position | undefined {
|
||||
let position = start;
|
||||
for (
|
||||
let line = position.line;
|
||||
line < document.lineCount && (flags.throughLineBreaks || line === start.line);
|
||||
line++
|
||||
) {
|
||||
position = document.validatePosition(position.with({ line }));
|
||||
const text = document.lineAt(position).text;
|
||||
const index = text.indexOf(str, position.character);
|
||||
if (index >= 0) {
|
||||
return position.with({ character: index });
|
||||
}
|
||||
position = position.with({ character: 0 }); // set at line begin for next iteration
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function searchBackward(
|
||||
str: string,
|
||||
document: TextDocument,
|
||||
start: Position,
|
||||
flags: {
|
||||
throughLineBreaks?: boolean;
|
||||
} = {
|
||||
throughLineBreaks: false,
|
||||
},
|
||||
): Position | undefined {
|
||||
let position = start;
|
||||
for (
|
||||
let line = position.line;
|
||||
line >= 0 && (flags.throughLineBreaks || line === start.line);
|
||||
line--
|
||||
) {
|
||||
position = document.validatePosition(position.with({ line }));
|
||||
const text = document.lineAt(position).text;
|
||||
const index = text.lastIndexOf(str, position.character);
|
||||
if (index >= 0) {
|
||||
return position.with({ character: index });
|
||||
}
|
||||
position = position.with({ character: +Infinity }); // set at line end for next iteration
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function maybeGetLeft(
|
||||
position: Position,
|
||||
{
|
||||
count = 1,
|
||||
throughLineBreaks,
|
||||
dontMove,
|
||||
}: { count?: number; throughLineBreaks?: boolean; dontMove?: boolean },
|
||||
) {
|
||||
return dontMove
|
||||
? position
|
||||
: throughLineBreaks
|
||||
? position.getOffsetThroughLineBreaks(-count)
|
||||
: position.getLeft(count);
|
||||
}
|
||||
export function maybeGetRight(
|
||||
position: Position,
|
||||
{
|
||||
count = 1,
|
||||
throughLineBreaks,
|
||||
dontMove,
|
||||
}: { count?: number; throughLineBreaks?: boolean; dontMove?: boolean },
|
||||
) {
|
||||
return dontMove
|
||||
? position
|
||||
: throughLineBreaks
|
||||
? position.getOffsetThroughLineBreaks(count)
|
||||
: position.getRight(count);
|
||||
}
|
||||
|
||||
export function searchPosition(
|
||||
str: string,
|
||||
document: TextDocument,
|
||||
start: Position,
|
||||
flags: SearchFlags = {
|
||||
direction: '>',
|
||||
includeCursor: true,
|
||||
throughLineBreaks: false,
|
||||
},
|
||||
): Position | undefined {
|
||||
if (flags.direction === '<') {
|
||||
start = maybeGetLeft(start, {
|
||||
dontMove: flags.includeCursor,
|
||||
throughLineBreaks: flags.throughLineBreaks,
|
||||
});
|
||||
return searchBackward(str, document, start, flags);
|
||||
} else {
|
||||
start = maybeGetRight(start, {
|
||||
dontMove: flags.includeCursor,
|
||||
throughLineBreaks: flags.throughLineBreaks,
|
||||
});
|
||||
return searchForward(str, document, start, flags);
|
||||
}
|
||||
}
|
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;
|
||||
}
|
||||
}
|
3
src/actions/plugins/targets/targets.ts
Normal file
3
src/actions/plugins/targets/targets.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// targets sub-plugins
|
||||
import './smartQuotes';
|
||||
import './lastNextObjects';
|
18
src/actions/plugins/targets/targetsConfig.ts
Normal file
18
src/actions/plugins/targets/targetsConfig.ts
Normal file
@ -0,0 +1,18 @@
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
export function bracketObjectsEnabled(): boolean {
|
||||
return (
|
||||
(configuration.targets.enable === true &&
|
||||
configuration.targets.bracketObjects.enable !== false) ||
|
||||
(configuration.targets.enable === undefined &&
|
||||
configuration.targets.bracketObjects.enable === true)
|
||||
);
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import { commands } from 'vscode';
|
||||
import { Action } from '../action_types';
|
||||
import { enterWindowMode } from '../modes';
|
||||
import { Mode } from '../modes_types';
|
||||
import { parseKeysExact } from '../parse_keys';
|
||||
|
||||
// https://docs.helix-editor.com/keymap.html#space-mode
|
||||
export const spaceActions: Action[] = [
|
||||
// Open File Picker
|
||||
parseKeysExact([' ', 'f'], [Mode.Normal], () => {
|
||||
commands.executeCommand('workbench.action.quickOpen');
|
||||
}),
|
||||
|
||||
parseKeysExact([' ', 'g'], [Mode.Normal], () => {
|
||||
commands.executeCommand('workbench.debug.action.focusBreakpointsView');
|
||||
}),
|
||||
|
||||
parseKeysExact([' ', 'k'], [Mode.Normal], () => {
|
||||
commands.executeCommand('editor.action.showHover');
|
||||
}),
|
||||
|
||||
parseKeysExact([' ', 's'], [Mode.Normal], () => {
|
||||
commands.executeCommand('workbench.action.gotoSymbol');
|
||||
}),
|
||||
|
||||
parseKeysExact([' ', 'S'], [Mode.Normal], () => {
|
||||
commands.executeCommand('workbench.action.showAllSymbols');
|
||||
}),
|
||||
|
||||
// View problems in current file
|
||||
parseKeysExact([' ', 'd'], [Mode.Normal], () => {
|
||||
commands.executeCommand('workbench.actions.view.problems');
|
||||
// It's not possible to set active file on and off, you can only toggle it, which makes implementing this difficult
|
||||
// For now both d and D will do the same thing and search all of the workspace
|
||||
|
||||
// Leaving this here for future reference
|
||||
// commands.executeCommand('workbench.actions.workbench.panel.markers.view.toggleActiveFile');
|
||||
}),
|
||||
|
||||
// View problems in workspace
|
||||
parseKeysExact([' ', 'D'], [Mode.Normal], () => {
|
||||
// alias of 'd'. See above
|
||||
commands.executeCommand('workbench.actions.view.problems');
|
||||
}),
|
||||
|
||||
parseKeysExact([' ', 'r'], [Mode.Normal], () => {
|
||||
commands.executeCommand('editor.action.rename');
|
||||
}),
|
||||
|
||||
parseKeysExact([' ', 'a'], [Mode.Normal], () => {
|
||||
commands.executeCommand('editor.action.quickFix');
|
||||
}),
|
||||
|
||||
parseKeysExact([' ', 'w'], [Mode.Normal], (helixState) => {
|
||||
enterWindowMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact([' ', '/'], [Mode.Normal], () => {
|
||||
commands.executeCommand('workbench.action.findInFiles');
|
||||
}),
|
||||
|
||||
parseKeysExact([' ', '?'], [Mode.Normal], () => {
|
||||
commands.executeCommand('workbench.action.showCommands');
|
||||
}),
|
||||
];
|
25
src/actions/types.d.ts
vendored
Normal file
25
src/actions/types.d.ts
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
import type { Position } from 'vscode';
|
||||
import type { VimState } from '../state/vimState';
|
||||
|
||||
export type ActionType = 'command' | 'motion' | 'operator' | 'number';
|
||||
|
||||
export interface IBaseAction {
|
||||
name: string;
|
||||
readonly actionType: ActionType;
|
||||
readonly isJump: boolean;
|
||||
readonly createsUndoPoint: boolean;
|
||||
|
||||
keysPressed: string[];
|
||||
multicursorIndex: number | undefined;
|
||||
|
||||
readonly preservesDesiredColumn: boolean;
|
||||
}
|
||||
|
||||
export interface IBaseCommand extends IBaseAction {
|
||||
exec(position: Position, vimState: VimState): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IBaseOperator extends IBaseAction {
|
||||
run(vimState: VimState, start: Position, stop: Position): Promise<void>;
|
||||
runRepeat(vimState: VimState, position: Position, count: number): Promise<void>;
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import { Selection, TextEditorRevealType, commands } from 'vscode';
|
||||
import { Action } from '../action_types';
|
||||
import { Mode } from '../modes_types';
|
||||
import { parseKeysExact } from '../parse_keys';
|
||||
|
||||
export const unimparedActions: Action[] = [
|
||||
parseKeysExact([']', 'D'], [Mode.Normal], () => {
|
||||
commands.executeCommand('editor.action.marker.next');
|
||||
}),
|
||||
|
||||
parseKeysExact(['[', 'D'], [Mode.Normal], () => {
|
||||
commands.executeCommand('editor.action.marker.prev');
|
||||
}),
|
||||
|
||||
parseKeysExact([']', 'd'], [Mode.Normal], () => {
|
||||
commands.executeCommand('editor.action.marker.nextInFiles');
|
||||
}),
|
||||
|
||||
parseKeysExact(['[', 'd'], [Mode.Normal], () => {
|
||||
commands.executeCommand('editor.action.marker.prevInFiles');
|
||||
}),
|
||||
|
||||
parseKeysExact(['[', 'g'], [Mode.Normal], () => {
|
||||
// There is no way to check if we're in compare editor mode or not so i need to call both commands
|
||||
commands.executeCommand('workbench.action.compareEditor.previousChange');
|
||||
commands.executeCommand('workbench.action.editor.previousChange');
|
||||
}),
|
||||
|
||||
parseKeysExact([']', 'g'], [Mode.Normal], () => {
|
||||
// There is no way to check if we're in compare editor mode or not so i need to call both commands
|
||||
commands.executeCommand('workbench.action.compareEditor.nextChange');
|
||||
commands.executeCommand('workbench.action.editor.nextChange');
|
||||
}),
|
||||
|
||||
parseKeysExact([']', 'f'], [Mode.Normal], (helixState, editor) => {
|
||||
const range = helixState.symbolProvider.getNextFunctionRange(editor);
|
||||
if (range) {
|
||||
editor.revealRange(range, TextEditorRevealType.InCenter);
|
||||
editor.selection = new Selection(range.start, range.end);
|
||||
}
|
||||
}),
|
||||
|
||||
parseKeysExact(['[', 'f'], [Mode.Normal], (helixState, editor) => {
|
||||
const range = helixState.symbolProvider.getPreviousFunctionRange(editor);
|
||||
if (range) {
|
||||
editor.revealRange(range, TextEditorRevealType.InCenter);
|
||||
editor.selection = new Selection(range.start, range.end);
|
||||
}
|
||||
}),
|
||||
];
|
@ -1,101 +0,0 @@
|
||||
import { commands } from 'vscode';
|
||||
import { Action } from '../action_types';
|
||||
import { enterViewMode } from '../modes';
|
||||
import { Mode } from '../modes_types';
|
||||
import { parseKeysExact } from '../parse_keys';
|
||||
|
||||
// https://docs.helix-editor.com/keymap.html#view-mode
|
||||
export const viewActions: Action[] = [
|
||||
parseKeysExact(['Z'], [Mode.Normal, Mode.Visual], (helixState) => {
|
||||
enterViewMode(helixState);
|
||||
}),
|
||||
|
||||
// align view center
|
||||
parseKeysExact(['z', 'c'], [Mode.Normal, Mode.Visual], (_, editor) => {
|
||||
commands.executeCommand('revealLine', {
|
||||
lineNumber: editor.selection.active.line,
|
||||
at: 'center',
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['c'], [Mode.View], (_, editor) => {
|
||||
commands.executeCommand('revealLine', {
|
||||
lineNumber: editor.selection.active.line,
|
||||
at: 'center',
|
||||
});
|
||||
}),
|
||||
|
||||
// align view top
|
||||
parseKeysExact(['z', 't'], [Mode.Normal, Mode.Visual], (_, editor) => {
|
||||
commands.executeCommand('revealLine', {
|
||||
lineNumber: editor.selection.active.line,
|
||||
at: 'top',
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['t'], [Mode.View], (_, editor) => {
|
||||
commands.executeCommand('revealLine', {
|
||||
lineNumber: editor.selection.active.line,
|
||||
at: 'top',
|
||||
});
|
||||
}),
|
||||
|
||||
// align view bottom
|
||||
parseKeysExact(['z', 'b'], [Mode.Normal, Mode.Visual], (_, editor) => {
|
||||
commands.executeCommand('revealLine', {
|
||||
lineNumber: editor.selection.active.line,
|
||||
at: 'bottom',
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['b'], [Mode.View], (_, editor) => {
|
||||
commands.executeCommand('revealLine', {
|
||||
lineNumber: editor.selection.active.line,
|
||||
at: 'bottom',
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['z', 't'], [Mode.Normal, Mode.Visual], (_, editor) => {
|
||||
commands.executeCommand('revealLine', {
|
||||
lineNumber: editor.selection.active.line,
|
||||
at: 'top',
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['t'], [Mode.View], (_, editor) => {
|
||||
commands.executeCommand('revealLine', {
|
||||
lineNumber: editor.selection.active.line,
|
||||
at: 'top',
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['z', 'j'], [Mode.Normal, Mode.Visual], () => {
|
||||
commands.executeCommand('scrollLineDown');
|
||||
}),
|
||||
|
||||
parseKeysExact(['j'], [Mode.View], () => {
|
||||
commands.executeCommand('scrollLineDown');
|
||||
}),
|
||||
|
||||
parseKeysExact(['z', 'k'], [Mode.Normal, Mode.Visual], () => {
|
||||
commands.executeCommand('scrollLineUp');
|
||||
}),
|
||||
|
||||
parseKeysExact(['k'], [Mode.View], () => {
|
||||
commands.executeCommand('scrollLineUp');
|
||||
}),
|
||||
|
||||
parseKeysExact(['z', 'z'], [Mode.Normal, Mode.Visual], (_, editor) => {
|
||||
commands.executeCommand('revealLine', {
|
||||
lineNumber: editor.selection.active.line,
|
||||
at: 'center',
|
||||
});
|
||||
}),
|
||||
|
||||
parseKeysExact(['z'], [Mode.View], (_, editor) => {
|
||||
commands.executeCommand('revealLine', {
|
||||
lineNumber: editor.selection.active.line,
|
||||
at: 'center',
|
||||
});
|
||||
}),
|
||||
];
|
@ -1,116 +0,0 @@
|
||||
import { commands } from 'vscode';
|
||||
import { Action } from '../action_types';
|
||||
import { enterNormalMode } from '../modes';
|
||||
import { Mode } from '../modes_types';
|
||||
import { parseKeysExact } from '../parse_keys';
|
||||
|
||||
// https://docs.helix-editor.com/keymap.html#window-mode
|
||||
export const windowActions: Action[] = [
|
||||
// New window modes (moving existing windows)
|
||||
parseKeysExact(['m', 'v'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.moveEditorToNextGroup');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['m', 's'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.moveEditorToBelowGroup');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['m', 'p'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.moveEditorToPreviousGroup');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['m', 'w'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.moveEditorToNewWindow');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['m', 'j'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.restoreEditorsToMainWindow');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
// Crtl+w actions
|
||||
parseKeysExact(['w'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.navigateEditorGroups');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['v'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.splitEditor');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['s'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.splitEditorDown');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['F'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('editor.action.revealDefinitionAside');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['f'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('editor.action.revealDefinitionAside');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['h'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.focusLeftGroup');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['l'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.focusRightGroup');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['j'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.focusBelowGroup');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['k'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.focusAboveGroup');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['q'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
// Alias q (for vim compatibility)
|
||||
parseKeysExact(['c'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['o'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.closeOtherEditors');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['H'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.moveActiveEditorGroupLeft');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['L'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.moveActiveEditorGroupRight');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['n'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.files.newUntitledFile');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
|
||||
parseKeysExact(['b'], [Mode.Window], (helixState) => {
|
||||
commands.executeCommand('workbench.action.toggleSidebarVisibility');
|
||||
enterNormalMode(helixState);
|
||||
}),
|
||||
];
|
25
src/actions/wrapping.ts
Normal file
25
src/actions/wrapping.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { configuration } from './../configuration/configuration';
|
||||
import { Mode } from './../mode/mode';
|
||||
|
||||
/**
|
||||
* See https://vimhelp.org/options.txt.html#%27whichwrap%27
|
||||
*
|
||||
* @returns true if the given key should cause the cursor to wrap around line boundary
|
||||
*/
|
||||
export const shouldWrapKey = (mode: Mode, key: string): boolean => {
|
||||
let k: string;
|
||||
if (key === '<left>') {
|
||||
k = [Mode.Insert, Mode.Replace].includes(mode) ? '[' : '<';
|
||||
} else if (key === '<right>') {
|
||||
k = [Mode.Insert, Mode.Replace].includes(mode) ? ']' : '>';
|
||||
} else if (['<BS>', '<C-BS>', '<S-BS>'].includes(key)) {
|
||||
k = 'b';
|
||||
} else if (key === ' ') {
|
||||
k = 's';
|
||||
} else if (['h', 'l', '~'].includes(key)) {
|
||||
k = key;
|
||||
} else {
|
||||
throw new Error(`shouldWrapKey called with unexpected key='${key}'`);
|
||||
}
|
||||
return configuration.whichwrap.split(',').includes(k);
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
export function arrayFindLast<T>(xs: T[], p: (x: T) => boolean): T | undefined {
|
||||
const filtered = xs.filter(p)
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return undefined
|
||||
} else {
|
||||
return filtered[filtered.length - 1]
|
||||
}
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
enum MatchType {
|
||||
start = 'start',
|
||||
end = 'end',
|
||||
}
|
||||
|
||||
interface BlockMatch {
|
||||
type: MatchType
|
||||
match: RegExpMatchArray
|
||||
}
|
||||
|
||||
const startRegex = (startWords: string[]) => RegExp(`(^|\\s)(${startWords.join('|')})($|\\s)`, 'g')
|
||||
|
||||
const endRegex = (endWords: string[]) => RegExp(`(^|\\s)(${endWords.join('|')})($|\\W)`, 'g')
|
||||
|
||||
export function blockRange(document: vscode.TextDocument, position: vscode.Position): vscode.Range {
|
||||
let startWords: string[] = []
|
||||
let endWords: string[] = []
|
||||
|
||||
console.log(`LanguageID=${document.languageId}`)
|
||||
if (document.languageId === 'elixir') {
|
||||
startWords = ['case', 'cond', 'fn', 'def']
|
||||
endWords = ['end']
|
||||
} else {
|
||||
console.log(`Unsupported language: ${document.languageId}`)
|
||||
return new vscode.Range(position, position)
|
||||
}
|
||||
|
||||
const start = findBlockStart(document, position, startWords, endWords)
|
||||
const end = findBlockEnd(document, position, startWords, endWords)
|
||||
|
||||
if (start && end) {
|
||||
return new vscode.Range(start, end)
|
||||
}
|
||||
return new vscode.Range(position, position)
|
||||
}
|
||||
|
||||
function findBlockStart(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
startWords: string[],
|
||||
endWords: string[],
|
||||
): vscode.Position | undefined {
|
||||
const closedBlocks: boolean[] = []
|
||||
|
||||
for (let i = position.line; i >= 0; --i) {
|
||||
const lineText =
|
||||
i === position.line
|
||||
? document.lineAt(i).text.substr(position.character)
|
||||
: document.lineAt(i).text
|
||||
|
||||
let blockMatches: BlockMatch[] = []
|
||||
for (const m of lineText.matchAll(startRegex(startWords))) {
|
||||
blockMatches.push({ type: MatchType.start, match: m })
|
||||
}
|
||||
|
||||
for (const m of lineText.matchAll(endRegex(endWords))) {
|
||||
blockMatches.push({ type: MatchType.end, match: m })
|
||||
}
|
||||
|
||||
blockMatches = blockMatches.sort((a, b) =>
|
||||
(a.match.index as number) > (b.match.index as number) ? 1 : -1,
|
||||
)
|
||||
|
||||
for (let idx = 0; idx < blockMatches.length; idx++) {
|
||||
const blockMatch = blockMatches[idx]
|
||||
if (blockMatch.type === MatchType.end) {
|
||||
closedBlocks.push(true)
|
||||
} else if (blockMatch.type === MatchType.start) {
|
||||
if (closedBlocks.length === 0) {
|
||||
const [fullText, , matchText] = blockMatch.match
|
||||
const offset = fullText.indexOf(matchText)
|
||||
return new vscode.Position(i, (blockMatch.match.index as number) + offset)
|
||||
} else {
|
||||
closedBlocks.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function findBlockEnd(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
startWords: string[],
|
||||
endWords: string[],
|
||||
): vscode.Position | undefined {
|
||||
const openedBlocks: boolean[] = [true]
|
||||
|
||||
for (let i = position.line; i < document.lineCount; ++i) {
|
||||
const lineText =
|
||||
i === position.line
|
||||
? document.lineAt(i).text.substr(position.character)
|
||||
: document.lineAt(i).text
|
||||
|
||||
let blockMatches: BlockMatch[] = []
|
||||
for (const m of lineText.matchAll(startRegex(startWords))) {
|
||||
blockMatches.push({ type: MatchType.start, match: m })
|
||||
}
|
||||
|
||||
for (const m of lineText.matchAll(endRegex(endWords))) {
|
||||
blockMatches.push({ type: MatchType.end, match: m })
|
||||
}
|
||||
|
||||
blockMatches = blockMatches.sort((a, b) =>
|
||||
(a.match.index as number) > (b.match.index as number) ? 1 : -1,
|
||||
)
|
||||
|
||||
for (let idx = 0; idx < blockMatches.length; idx++) {
|
||||
const blockMatch = blockMatches[idx]
|
||||
if (blockMatch.type === MatchType.start) {
|
||||
openedBlocks.push(true)
|
||||
} else if (blockMatch.type === MatchType.end) {
|
||||
openedBlocks.pop()
|
||||
if (openedBlocks.length === 0) {
|
||||
const [fullText, , matchText] = blockMatch.match
|
||||
const offset = fullText.indexOf(matchText)
|
||||
return new vscode.Position(
|
||||
i,
|
||||
(blockMatch.match.index as number) + offset + matchText.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
549
src/cmd_line/commandLine.ts
Normal file
549
src/cmd_line/commandLine.ts
Normal file
@ -0,0 +1,549 @@
|
||||
import { Parser } from 'parsimmon';
|
||||
import { ExtensionContext, Position, window } from 'vscode';
|
||||
import { configuration } from '../configuration/configuration';
|
||||
import { ErrorCode, VimError } from '../error';
|
||||
import { CommandLineHistory, HistoryFile, SearchHistory } from '../history/historyFile';
|
||||
import { Register } from '../register/register';
|
||||
import { globalState } from '../state/globalState';
|
||||
import { RecordedState } from '../state/recordedState';
|
||||
import { IndexedPosition, IndexedRange, SearchState } from '../state/searchState';
|
||||
import { VimState } from '../state/vimState';
|
||||
import { StatusBar } from '../statusBar';
|
||||
import { WordType, getWordLeftInText, getWordRightInText } from '../textobject/word';
|
||||
import { SearchDecorations, getDecorationsForSearchMatchRanges } from '../util/decorationUtils';
|
||||
import { Logger } from '../util/logger';
|
||||
import { escapeCSSIcons, reportSearch } from '../util/statusBarTextUtils';
|
||||
import { scrollView } from '../util/util';
|
||||
import { ExCommand } from '../vimscript/exCommand';
|
||||
import { LineRange } from '../vimscript/lineRange';
|
||||
import { SearchDirection } from '../vimscript/pattern';
|
||||
import { Mode } from './../mode/mode';
|
||||
import { RegisterCommand } from './commands/register';
|
||||
import { SubstituteCommand } from './commands/substitute';
|
||||
|
||||
export abstract class CommandLine {
|
||||
public cursorIndex: number;
|
||||
public previousMode: Mode;
|
||||
protected historyIndex: number | undefined;
|
||||
private savedText: string;
|
||||
|
||||
constructor(text: string, previousMode: Mode) {
|
||||
this.cursorIndex = text.length;
|
||||
this.historyIndex = this.getHistory().get().length;
|
||||
this.previousMode = previousMode;
|
||||
this.savedText = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the text to be displayed in the status bar
|
||||
*/
|
||||
public abstract display(cursorChar: string): string;
|
||||
|
||||
/**
|
||||
* What the user has typed, minus any prefix, etc.
|
||||
*/
|
||||
public abstract get text(): string;
|
||||
public abstract set text(text: string);
|
||||
|
||||
/**
|
||||
* @returns the SearchState associated with this CommandLine, if one exists
|
||||
*
|
||||
* This applies to `/`, `:s`, `:g`, `:v`, etc.
|
||||
*/
|
||||
public abstract getSearchState(): SearchState | undefined;
|
||||
|
||||
public abstract getHistory(): HistoryFile;
|
||||
|
||||
public abstract getDecorations(vimState: VimState): SearchDecorations | undefined;
|
||||
|
||||
/**
|
||||
* Called when `<Enter>` is pressed
|
||||
*/
|
||||
public abstract run(vimState: VimState): Promise<void>;
|
||||
|
||||
/**
|
||||
* Called when `<Esc>` is pressed
|
||||
*/
|
||||
public abstract escape(vimState: VimState): Promise<void>;
|
||||
|
||||
/**
|
||||
* Called when `<C-f>` is pressed
|
||||
*/
|
||||
public abstract ctrlF(vimState: VimState): Promise<void>;
|
||||
|
||||
public async historyBack(): Promise<void> {
|
||||
if (this.historyIndex === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const historyEntries = this.getHistory().get();
|
||||
if (this.historyIndex === undefined) {
|
||||
this.historyIndex = historyEntries.length - 1;
|
||||
this.savedText = this.text;
|
||||
} else if (this.historyIndex > 0) {
|
||||
this.historyIndex--;
|
||||
}
|
||||
|
||||
this.text = historyEntries[this.historyIndex];
|
||||
this.cursorIndex = this.text.length;
|
||||
}
|
||||
|
||||
public async historyForward(): Promise<void> {
|
||||
if (this.historyIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const historyEntries = this.getHistory().get();
|
||||
if (this.historyIndex === historyEntries.length - 1) {
|
||||
this.historyIndex = undefined;
|
||||
this.text = this.savedText;
|
||||
} else if (this.historyIndex < historyEntries.length - 1) {
|
||||
this.historyIndex++;
|
||||
this.text = historyEntries[this.historyIndex];
|
||||
}
|
||||
|
||||
this.cursorIndex = this.text.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when `<BS>` is pressed
|
||||
*/
|
||||
public async backspace(vimState: VimState): Promise<void> {
|
||||
if (this.cursorIndex === 0) {
|
||||
if (this.text.length === 0) {
|
||||
await this.escape(vimState);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.text = this.text.slice(0, this.cursorIndex - 1) + this.text.slice(this.cursorIndex);
|
||||
this.cursorIndex = Math.max(this.cursorIndex - 1, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when `<Del>` is pressed
|
||||
*/
|
||||
public async delete(vimState: VimState): Promise<void> {
|
||||
if (this.cursorIndex === this.text.length) {
|
||||
return this.backspace(vimState);
|
||||
}
|
||||
|
||||
this.text = this.text.slice(0, this.cursorIndex) + this.text.slice(this.cursorIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when `<Home>` is pressed
|
||||
*/
|
||||
public async home(): Promise<void> {
|
||||
this.cursorIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when `<End>` is pressed
|
||||
*/
|
||||
public async end(): Promise<void> {
|
||||
this.cursorIndex = this.text.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when `<C-Left>` is pressed
|
||||
*/
|
||||
public async wordLeft(): Promise<void> {
|
||||
this.cursorIndex = getWordLeftInText(this.text, this.cursorIndex, WordType.Big) ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when `<C-Right>` is pressed
|
||||
*/
|
||||
public async wordRight(): Promise<void> {
|
||||
this.cursorIndex =
|
||||
getWordRightInText(this.text, this.cursorIndex, WordType.Big) ?? this.text.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when `<C-BS>` is pressed
|
||||
*/
|
||||
public async deleteWord(): Promise<void> {
|
||||
const wordStart = getWordLeftInText(this.text, this.cursorIndex, WordType.Normal);
|
||||
if (wordStart !== undefined) {
|
||||
this.text = this.text.substring(0, wordStart).concat(this.text.slice(this.cursorIndex));
|
||||
this.cursorIndex = this.cursorIndex - (this.cursorIndex - wordStart);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when `<C-BS>` is pressed
|
||||
*/
|
||||
public async deleteToBeginning(): Promise<void> {
|
||||
this.text = this.text.slice(this.cursorIndex);
|
||||
this.cursorIndex = 0;
|
||||
}
|
||||
|
||||
public async typeCharacter(char: string): Promise<void> {
|
||||
const modifiedString = this.text.split('');
|
||||
modifiedString.splice(this.cursorIndex, 0, char);
|
||||
this.text = modifiedString.join('');
|
||||
this.cursorIndex += char.length;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExCommandLine extends CommandLine {
|
||||
static history: CommandLineHistory;
|
||||
static parser: Parser<{ lineRange: LineRange | undefined; command: ExCommand }>;
|
||||
static onSearch: (vimState: VimState) => Promise<void>;
|
||||
|
||||
public static async loadHistory(context: ExtensionContext): Promise<void> {
|
||||
ExCommandLine.history = new CommandLineHistory(context);
|
||||
await ExCommandLine.history.load();
|
||||
}
|
||||
|
||||
// TODO: Make this stuff private?
|
||||
public autoCompleteIndex = 0;
|
||||
public autoCompleteItems: string[] = [];
|
||||
public preCompleteCharacterPos = 0;
|
||||
public preCompleteCommand = '';
|
||||
|
||||
private commandText: string;
|
||||
private lineRange: LineRange | undefined;
|
||||
private command: ExCommand | undefined;
|
||||
|
||||
constructor(commandText: string, previousMode: Mode) {
|
||||
super(commandText, previousMode);
|
||||
this.commandText = commandText;
|
||||
this.text = commandText;
|
||||
this.previousMode = previousMode;
|
||||
}
|
||||
|
||||
public display(cursorChar: string): string {
|
||||
return escapeCSSIcons(
|
||||
`:${this.text.substring(0, this.cursorIndex)}${cursorChar}${this.text.substring(
|
||||
this.cursorIndex,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
public get text(): string {
|
||||
return this.commandText;
|
||||
}
|
||||
public set text(text: string) {
|
||||
this.commandText = text;
|
||||
|
||||
try {
|
||||
// TODO: This eager parsing is costly, and if it's not `:s` or similar, don't need to parse the args at all
|
||||
const { lineRange, command } = ExCommandLine.parser.tryParse(this.commandText);
|
||||
this.lineRange = lineRange;
|
||||
this.command = command;
|
||||
} catch (err) {
|
||||
this.lineRange = undefined;
|
||||
this.command = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public getSearchState(): SearchState | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getDecorations(vimState: VimState): SearchDecorations | undefined {
|
||||
return this.command instanceof SubstituteCommand &&
|
||||
vimState.currentMode === Mode.CommandlineInProgress
|
||||
? this.command.getSubstitutionDecorations(vimState, this.lineRange)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
public getHistory(): HistoryFile {
|
||||
return ExCommandLine.history;
|
||||
}
|
||||
|
||||
public async run(vimState: VimState): Promise<void> {
|
||||
Logger.info(`Executing :${this.text}`);
|
||||
void ExCommandLine.history.add(this.text);
|
||||
this.historyIndex = ExCommandLine.history.get().length;
|
||||
|
||||
if (!(this.command instanceof RegisterCommand)) {
|
||||
// TODO(jfields): Wait...why are we saving the `:` register as a RecordedState?
|
||||
const recState = new RecordedState();
|
||||
recState.registerName = ':';
|
||||
recState.commandList = this.text.split('');
|
||||
Register.setReadonlyRegister(':', recState);
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.command === undefined) {
|
||||
// TODO: A bit gross:
|
||||
ExCommandLine.parser.tryParse(this.text);
|
||||
throw new Error(`Expected parsing ExCommand '${this.text}' to fail`);
|
||||
}
|
||||
|
||||
const useNeovim = configuration.enableNeovim && this.command.neovimCapable();
|
||||
if (useNeovim && vimState.nvim) {
|
||||
const { statusBarText, error } = await vimState.nvim.run(vimState, this.text);
|
||||
StatusBar.setText(vimState, statusBarText, error);
|
||||
} else {
|
||||
if (this.lineRange) {
|
||||
await this.command.executeWithRange(vimState, this.lineRange);
|
||||
} else {
|
||||
await this.command.execute(vimState);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof VimError) {
|
||||
if (
|
||||
e.code === ErrorCode.NotAnEditorCommand &&
|
||||
configuration.enableNeovim &&
|
||||
vimState.nvim
|
||||
) {
|
||||
const { statusBarText } = await vimState.nvim.run(vimState, this.text);
|
||||
StatusBar.setText(vimState, statusBarText, true);
|
||||
} else {
|
||||
StatusBar.setText(vimState, e.toString(), true);
|
||||
}
|
||||
} else {
|
||||
Logger.error(`Error executing cmd=${this.text}. err=${e}.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Update state if this command is repeatable via dot command.
|
||||
vimState.lastCommandDotRepeatable = this.command?.isRepeatableWithDot ?? false;
|
||||
}
|
||||
|
||||
public async escape(vimState: VimState): Promise<void> {
|
||||
await vimState.setCurrentMode(Mode.Normal);
|
||||
if (this.text.length > 0) {
|
||||
void ExCommandLine.history.add(this.text);
|
||||
}
|
||||
}
|
||||
|
||||
public async ctrlF(vimState: VimState): Promise<void> {
|
||||
void ExCommandLine.onSearch(vimState);
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchCommandLine extends CommandLine {
|
||||
public static history: SearchHistory;
|
||||
public static readonly previousSearchStates: SearchState[] = [];
|
||||
public static onSearch: (vimState: VimState, direction: SearchDirection) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Shows the search history as a QuickPick (popup list)
|
||||
*
|
||||
* @returns The SearchState that was selected by the user, if there was one.
|
||||
*/
|
||||
public static async showSearchHistory(): Promise<SearchState | undefined> {
|
||||
const items = SearchCommandLine.previousSearchStates
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((searchState) => {
|
||||
return {
|
||||
label: searchState.searchString,
|
||||
searchState,
|
||||
};
|
||||
});
|
||||
|
||||
const item = await window.showQuickPick(items, {
|
||||
placeHolder: 'Vim search history',
|
||||
ignoreFocusOut: false,
|
||||
});
|
||||
|
||||
return item?.searchState;
|
||||
}
|
||||
|
||||
public static async loadHistory(context: ExtensionContext): Promise<void> {
|
||||
SearchCommandLine.history = new SearchHistory(context);
|
||||
SearchCommandLine.history
|
||||
.get()
|
||||
.forEach((val) =>
|
||||
SearchCommandLine.previousSearchStates.push(
|
||||
new SearchState(SearchDirection.Forward, new Position(0, 0), val, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public static async addSearchStateToHistory(searchState: SearchState) {
|
||||
const prevSearchString =
|
||||
SearchCommandLine.previousSearchStates.length === 0
|
||||
? undefined
|
||||
: SearchCommandLine.previousSearchStates[SearchCommandLine.previousSearchStates.length - 1]
|
||||
.searchString;
|
||||
// Store this search if different than previous
|
||||
if (searchState.searchString !== prevSearchString) {
|
||||
SearchCommandLine.previousSearchStates.push(searchState);
|
||||
if (SearchCommandLine.history !== undefined) {
|
||||
await SearchCommandLine.history.add(searchState.searchString);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure search history does not exceed configuration option
|
||||
if (SearchCommandLine.previousSearchStates.length > configuration.history) {
|
||||
SearchCommandLine.previousSearchStates.splice(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the state of the current match, i.e. the match to which the cursor moves when the search is executed.
|
||||
* Incremented / decremented by \<C-g> or \<C-t> in SearchInProgress mode.
|
||||
* Resets to 0 if the search string becomes empty.
|
||||
*
|
||||
* @see {@link getCurrentMatchRelativeIndex}
|
||||
*/
|
||||
private currentMatchDisplacement: number = 0;
|
||||
private searchState: SearchState;
|
||||
|
||||
constructor(vimState: VimState, searchString: string, direction: SearchDirection) {
|
||||
super(searchString, vimState.currentMode);
|
||||
this.searchState = new SearchState(direction, vimState.cursorStopPosition, searchString);
|
||||
}
|
||||
|
||||
public display(cursorChar: string): string {
|
||||
return escapeCSSIcons(
|
||||
`${this.searchState.direction === SearchDirection.Forward ? '/' : '?'}${this.text.substring(
|
||||
0,
|
||||
this.cursorIndex,
|
||||
)}${cursorChar}${this.text.substring(this.cursorIndex)}`,
|
||||
);
|
||||
}
|
||||
|
||||
public get text(): string {
|
||||
return this.searchState.searchString;
|
||||
}
|
||||
public set text(text: string) {
|
||||
this.searchState.searchString = text;
|
||||
if (text === '') {
|
||||
this.currentMatchDisplacement = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public getSearchState(): SearchState {
|
||||
return this.searchState;
|
||||
}
|
||||
|
||||
public getHistory(): HistoryFile {
|
||||
return SearchCommandLine.history;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the index of the current match, relative to the next match.
|
||||
*/
|
||||
private getCurrentMatchRelativeIndex(vimState: VimState): number {
|
||||
const count = vimState.recordedState.count || 1;
|
||||
return count - 1 + this.currentMatchDisplacement * count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The start of the current match range (after applying the search offset) and its rank in the document's matches
|
||||
*/
|
||||
public getCurrentMatchPosition(vimState: VimState): IndexedPosition | undefined {
|
||||
return this.searchState.getNextSearchMatchPosition(
|
||||
vimState,
|
||||
vimState.cursorStopPosition,
|
||||
SearchDirection.Forward,
|
||||
this.getCurrentMatchRelativeIndex(vimState),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The current match range and its rank in the document's matches
|
||||
*
|
||||
* NOTE: This method does not take the search offset into account
|
||||
*/
|
||||
public getCurrentMatchRange(vimState: VimState): IndexedRange | undefined {
|
||||
return this.searchState.getNextSearchMatchRange(
|
||||
vimState,
|
||||
vimState.cursorStopPosition,
|
||||
SearchDirection.Forward,
|
||||
this.getCurrentMatchRelativeIndex(vimState),
|
||||
);
|
||||
}
|
||||
|
||||
public getDecorations(vimState: VimState): SearchDecorations | undefined {
|
||||
return getDecorationsForSearchMatchRanges(
|
||||
this.searchState.getMatchRanges(vimState),
|
||||
configuration.incsearch && vimState.currentMode === Mode.SearchInProgressMode
|
||||
? this.getCurrentMatchRange(vimState)?.index
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
public async run(vimState: VimState): Promise<void> {
|
||||
// Repeat the previous search if no new string is entered
|
||||
if (this.text === '') {
|
||||
if (SearchCommandLine.previousSearchStates.length > 0) {
|
||||
this.text =
|
||||
SearchCommandLine.previousSearchStates[
|
||||
SearchCommandLine.previousSearchStates.length - 1
|
||||
].searchString;
|
||||
}
|
||||
}
|
||||
Logger.info(`Searching for ${this.text}`);
|
||||
|
||||
this.cursorIndex = 0;
|
||||
Register.setReadonlyRegister('/', this.text);
|
||||
void SearchCommandLine.addSearchStateToHistory(this.searchState);
|
||||
globalState.hl = true;
|
||||
|
||||
if (this.searchState.getMatchRanges(vimState).length === 0) {
|
||||
StatusBar.displayError(vimState, VimError.fromCode(ErrorCode.PatternNotFound, this.text));
|
||||
return;
|
||||
}
|
||||
|
||||
const currentMatch = this.getCurrentMatchPosition(vimState);
|
||||
|
||||
if (currentMatch === undefined) {
|
||||
StatusBar.displayError(
|
||||
vimState,
|
||||
VimError.fromCode(
|
||||
this.searchState.direction === SearchDirection.Backward
|
||||
? ErrorCode.SearchHitTop
|
||||
: ErrorCode.SearchHitBottom,
|
||||
this.text,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
vimState.cursorStopPosition = currentMatch.pos;
|
||||
|
||||
reportSearch(currentMatch.index, this.searchState.getMatchRanges(vimState).length, vimState);
|
||||
}
|
||||
|
||||
public async escape(vimState: VimState): Promise<void> {
|
||||
vimState.cursorStopPosition = this.searchState.cursorStartPosition;
|
||||
|
||||
const prevSearchList = SearchCommandLine.previousSearchStates;
|
||||
globalState.searchState = prevSearchList
|
||||
? prevSearchList[prevSearchList.length - 1]
|
||||
: undefined;
|
||||
|
||||
if (vimState.modeData.mode === Mode.SearchInProgressMode) {
|
||||
const offset =
|
||||
vimState.editor.visibleRanges[0].start.line -
|
||||
vimState.modeData.firstVisibleLineBeforeSearch;
|
||||
scrollView(vimState, offset);
|
||||
}
|
||||
|
||||
await vimState.setCurrentMode(this.previousMode);
|
||||
if (this.text.length > 0) {
|
||||
void SearchCommandLine.addSearchStateToHistory(this.searchState);
|
||||
}
|
||||
}
|
||||
|
||||
public async ctrlF(vimState: VimState): Promise<void> {
|
||||
await SearchCommandLine.onSearch(vimState, this.searchState.direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when <C-g> or <C-t> is pressed during SearchInProgress mode
|
||||
*/
|
||||
public async advanceCurrentMatch(vimState: VimState, direction: SearchDirection): Promise<void> {
|
||||
// <C-g> always moves forward in the document, and <C-t> always moves back, regardless of search direction.
|
||||
// To compensate, multiply the desired direction by the searchState's direction, so that
|
||||
// effectiveDirection == direction * (searchState.direction)^2 == direction.
|
||||
this.currentMatchDisplacement += this.searchState.direction * direction;
|
||||
|
||||
// With nowrapscan, <C-g>/<C-t> shouldn't do anything if it would mean advancing past the last reachable match in the buffer.
|
||||
// We account for this by checking whether getCurrentMatchRange returns undefined once this.currentMatchDisplacement is advanced.
|
||||
// If it does, we undo the change to this.currentMatchDisplacement before exiting, making this command a noop.
|
||||
if (!configuration.wrapscan && !this.getCurrentMatchRange(vimState)) {
|
||||
this.currentMatchDisplacement -= this.searchState.direction * direction;
|
||||
}
|
||||
}
|
||||
}
|
9
src/cmd_line/commands/ascii.ts
Normal file
9
src/cmd_line/commands/ascii.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { CommandUnicodeName } from '../../actions/commands/actions';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
|
||||
export class AsciiCommand extends ExCommand {
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
await new CommandUnicodeName().exec(vimState.cursorStopPosition, vimState);
|
||||
}
|
||||
}
|
63
src/cmd_line/commands/bang.ts
Normal file
63
src/cmd_line/commands/bang.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { PositionDiff } from '../../common/motion/position';
|
||||
import { externalCommand } from '../../util/externalCommand';
|
||||
import { LineRange } from '../../vimscript/lineRange';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { all, Parser } from 'parsimmon';
|
||||
|
||||
export interface IBangCommandArguments {
|
||||
command: string;
|
||||
}
|
||||
|
||||
export class BangCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<BangCommand> = all.map(
|
||||
(command) =>
|
||||
new BangCommand({
|
||||
command,
|
||||
}),
|
||||
);
|
||||
|
||||
protected _arguments: IBangCommandArguments;
|
||||
constructor(args: IBangCommandArguments) {
|
||||
super();
|
||||
this._arguments = args;
|
||||
}
|
||||
|
||||
public override neovimCapable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
private getReplaceDiff(text: string): PositionDiff {
|
||||
const lines = text.split('\n');
|
||||
const numNewlines = lines.length - 1;
|
||||
const check = lines[0].match(/^\s*/);
|
||||
const numWhitespace = check ? check[0].length : 0;
|
||||
|
||||
return PositionDiff.exactCharacter({
|
||||
lineOffset: -numNewlines,
|
||||
character: numWhitespace,
|
||||
});
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
await externalCommand.run(this._arguments.command);
|
||||
}
|
||||
|
||||
override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
const resolvedRange = range.resolveToRange(vimState);
|
||||
|
||||
// pipe in stdin from lines in range
|
||||
const input = vimState.document.getText(resolvedRange);
|
||||
const output = await externalCommand.run(this._arguments.command, input);
|
||||
|
||||
// place cursor at the start of the replaced text and first non-whitespace character
|
||||
const diff = this.getReplaceDiff(output);
|
||||
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'replaceText',
|
||||
text: output,
|
||||
range: resolvedRange,
|
||||
diff,
|
||||
});
|
||||
}
|
||||
}
|
249
src/cmd_line/commands/breakpoints.ts
Normal file
249
src/cmd_line/commands/breakpoints.ts
Normal file
@ -0,0 +1,249 @@
|
||||
import {
|
||||
all,
|
||||
alt,
|
||||
eof,
|
||||
optWhitespace,
|
||||
regexp,
|
||||
seqObj,
|
||||
// eslint-disable-next-line id-denylist
|
||||
string,
|
||||
succeed,
|
||||
whitespace,
|
||||
} from 'parsimmon';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { fileNameParser, numberParser } from '../../vimscript/parserUtils';
|
||||
|
||||
function isSourceBreakpoint(b: vscode.Breakpoint): b is vscode.SourceBreakpoint {
|
||||
return (b as vscode.SourceBreakpoint).location !== undefined;
|
||||
}
|
||||
function isFunctionBreakpoint(b: vscode.Breakpoint): b is vscode.FunctionBreakpoint {
|
||||
return (b as vscode.FunctionBreakpoint).functionName !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Breakpoint Command
|
||||
*/
|
||||
type AddBreakpointHere = { type: 'here' };
|
||||
type AddBreakpointFile = { type: 'file'; line: number; file: string };
|
||||
type AddBreakpointFunction = { type: 'func'; function: string };
|
||||
type AddBreakpointExpr = { type: 'expr'; expr: string };
|
||||
type AddBreakpoint =
|
||||
| AddBreakpointHere
|
||||
| AddBreakpointFile
|
||||
| AddBreakpointFunction
|
||||
| AddBreakpointExpr;
|
||||
|
||||
class AddBreakpointCommand extends ExCommand {
|
||||
public override isRepeatableWithDot: boolean = false;
|
||||
private readonly addBreakpoint: AddBreakpoint;
|
||||
|
||||
constructor(addBreakpoint: AddBreakpoint) {
|
||||
super();
|
||||
this.addBreakpoint = addBreakpoint;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
if (this.addBreakpoint.type === 'here') {
|
||||
const location = new vscode.Location(vimState.document.uri, vimState.cursorStartPosition);
|
||||
return vscode.debug.addBreakpoints([new vscode.SourceBreakpoint(location)]);
|
||||
} else if (this.addBreakpoint.type === 'file') {
|
||||
let file: vscode.Uri;
|
||||
if (this.addBreakpoint.file === '') {
|
||||
file = vimState.document.uri;
|
||||
} else {
|
||||
const workspaceFolder =
|
||||
vscode.workspace.getWorkspaceFolder(vimState.document.uri)?.uri ?? vscode.Uri.file('./');
|
||||
file = vscode.Uri.joinPath(workspaceFolder, this.addBreakpoint.file);
|
||||
}
|
||||
const location = new vscode.Location(
|
||||
file,
|
||||
new vscode.Position(this.addBreakpoint.line - 1, 0),
|
||||
);
|
||||
return vscode.debug.addBreakpoints([new vscode.SourceBreakpoint(location)]);
|
||||
} else if (this.addBreakpoint.type === 'func') {
|
||||
return vscode.debug.addBreakpoints([
|
||||
new vscode.FunctionBreakpoint(this.addBreakpoint.function),
|
||||
]);
|
||||
} else if (this.addBreakpoint.type === 'expr') {
|
||||
const location = new vscode.Location(vimState.document.uri, vimState.cursorStartPosition);
|
||||
return vscode.debug.addBreakpoints([
|
||||
new vscode.SourceBreakpoint(location, undefined, this.addBreakpoint.expr),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Breakpoint Command
|
||||
*/
|
||||
type DelBreakpointById = { type: 'byId'; id: number };
|
||||
type DelAllBreakpoints = { type: 'all' };
|
||||
type DelBreakpointFunction = { type: 'func'; function: string };
|
||||
type DelBreakpointFile = { type: 'file'; line: number; file: string };
|
||||
type DelBreakpointHere = { type: 'here' };
|
||||
type DelBreakpoint =
|
||||
| DelBreakpointById
|
||||
| DelAllBreakpoints
|
||||
| DelBreakpointFunction
|
||||
| DelBreakpointFile
|
||||
| DelBreakpointHere;
|
||||
|
||||
class DeleteBreakpointCommand extends ExCommand {
|
||||
public override isRepeatableWithDot: boolean = false;
|
||||
private readonly delBreakpoint: DelBreakpoint;
|
||||
|
||||
constructor(delBreakpoint: DelBreakpoint) {
|
||||
super();
|
||||
this.delBreakpoint = delBreakpoint;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
if (this.delBreakpoint.type === 'byId') {
|
||||
return vscode.debug.removeBreakpoints(
|
||||
vscode.debug.breakpoints.slice(this.delBreakpoint.id - 1, 1),
|
||||
);
|
||||
} else if (this.delBreakpoint.type === 'all') {
|
||||
return vscode.debug.removeBreakpoints(vscode.debug.breakpoints);
|
||||
} else if (this.delBreakpoint.type === 'file') {
|
||||
let reqUri: vscode.Uri;
|
||||
if (this.delBreakpoint.file === '') {
|
||||
reqUri = vimState.document.uri;
|
||||
} else {
|
||||
const workspaceFolder =
|
||||
vscode.workspace.getWorkspaceFolder(vimState.document.uri)?.uri ?? vscode.Uri.file('./');
|
||||
reqUri = vscode.Uri.joinPath(workspaceFolder, this.delBreakpoint.file);
|
||||
}
|
||||
const reqLine = this.delBreakpoint.line - 1;
|
||||
const breakpoint = vscode.debug.breakpoints
|
||||
.filter(isSourceBreakpoint)
|
||||
.find(
|
||||
(b) =>
|
||||
b.location.uri.toString() === reqUri.toString() &&
|
||||
b.location.range.start.line === reqLine,
|
||||
);
|
||||
if (breakpoint) return vscode.debug.removeBreakpoints([breakpoint]);
|
||||
} else if (this.delBreakpoint.type === 'func') {
|
||||
const functionName = this.delBreakpoint.function;
|
||||
const breakpoint = vscode.debug.breakpoints
|
||||
.filter(isFunctionBreakpoint)
|
||||
.filter((b) => b.functionName === functionName);
|
||||
if (breakpoint) return vscode.debug.removeBreakpoints(breakpoint);
|
||||
} else if (this.delBreakpoint.type === 'here') {
|
||||
const location = new vscode.Location(vimState.document.uri, vimState.cursorStartPosition);
|
||||
const distFromLocationCharacter = (b: vscode.SourceBreakpoint) =>
|
||||
Math.abs(b.location.range.start.character - location.range.start.character);
|
||||
|
||||
const breakpoint = vscode.debug.breakpoints
|
||||
.filter(isSourceBreakpoint)
|
||||
.filter(
|
||||
(b) =>
|
||||
b.location.uri.toString() === location.uri.toString() &&
|
||||
b.location.range.start.line === location.range.start.line,
|
||||
)
|
||||
.sort((a, b) => distFromLocationCharacter(a) - distFromLocationCharacter(b))[0];
|
||||
if (breakpoint) return vscode.debug.removeBreakpoints([breakpoint]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List Breakpoints Command
|
||||
*/
|
||||
class ListBreakpointsCommand extends ExCommand {
|
||||
public override isRepeatableWithDot: boolean = false;
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
const breakpoints = vscode.debug.breakpoints;
|
||||
type BreakpointQuickPick = { breakpointId: string } & vscode.QuickPickItem;
|
||||
const lines = breakpoints.map((b, i): BreakpointQuickPick => {
|
||||
const { id, enabled, condition } = b;
|
||||
let label = '';
|
||||
label += `#${i + 1}\t`;
|
||||
label += enabled ? '$(circle-filled)\t' : '$(circle-outline)\t';
|
||||
label += condition ? '$(debug-breakpoint-conditional)\t' : '\t';
|
||||
if (isSourceBreakpoint(b))
|
||||
label += `${path.basename(b.location.uri.fsPath)}:${b.location.range.start.line + 1}`;
|
||||
if (isFunctionBreakpoint(b)) label += `$(debug-breakpoint-function)${b.functionName}`;
|
||||
return {
|
||||
label,
|
||||
breakpointId: id,
|
||||
};
|
||||
});
|
||||
await vscode.window.showQuickPick(lines).then(async (selected) => {
|
||||
if (selected) {
|
||||
const id = selected.breakpointId;
|
||||
const breakpoint = breakpoints.find((b) => b.id === id);
|
||||
if (breakpoint && isSourceBreakpoint(breakpoint)) {
|
||||
await vscode.window.showTextDocument(breakpoint.location.uri).then(() => {
|
||||
vimState.cursorStopPosition = breakpoint.location.range.start;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Breakpoints {
|
||||
public static readonly argParsers = {
|
||||
add: whitespace
|
||||
.then(
|
||||
alt(
|
||||
// here
|
||||
seqObj<AddBreakpointHere>(['type', string('here')], optWhitespace),
|
||||
// file
|
||||
seqObj<AddBreakpointFile>(
|
||||
['type', string('file')],
|
||||
['line', optWhitespace.then(numberParser).fallback(1)],
|
||||
['file', optWhitespace.then(fileNameParser).fallback('')],
|
||||
),
|
||||
// func
|
||||
seqObj<AddBreakpointFunction>(
|
||||
['type', string('func')],
|
||||
optWhitespace.then(numberParser).fallback(1), // we don't support line numbers in function names, but Vim does, so we'll allow it.
|
||||
['function', optWhitespace.then(regexp(/\S+/))],
|
||||
),
|
||||
// expr
|
||||
seqObj<AddBreakpointExpr>(['type', string('expr')], ['expr', optWhitespace.then(all)]),
|
||||
),
|
||||
)
|
||||
.or(
|
||||
// without arg
|
||||
eof.result<DelBreakpointHere>({ type: 'here' }),
|
||||
)
|
||||
.map((a: AddBreakpoint) => new AddBreakpointCommand(a)),
|
||||
|
||||
del: whitespace
|
||||
.then(
|
||||
alt(
|
||||
// here
|
||||
seqObj<DelBreakpointHere>(['type', string('here')], optWhitespace),
|
||||
// file
|
||||
seqObj<DelBreakpointFile>(
|
||||
['type', string('file')],
|
||||
['line', optWhitespace.then(numberParser).fallback(1)],
|
||||
['file', optWhitespace.then(fileNameParser).fallback('')],
|
||||
),
|
||||
// func
|
||||
seqObj<DelBreakpointFunction>(
|
||||
['type', string('func')],
|
||||
optWhitespace.then(numberParser).fallback(1), // we don't support line numbers in function names, but Vim does, so we'll allow it.
|
||||
['function', optWhitespace.then(regexp(/\S+/))],
|
||||
),
|
||||
// all
|
||||
string('*').then(optWhitespace).result<DelAllBreakpoints>({ type: 'all' }),
|
||||
// by number
|
||||
numberParser.map((n) => ({ type: 'byId', id: n })),
|
||||
),
|
||||
)
|
||||
.or(
|
||||
// without arg
|
||||
eof.result<DelBreakpointHere>({ type: 'here' }),
|
||||
)
|
||||
.map((a: DelBreakpoint) => new DeleteBreakpointCommand(a)),
|
||||
|
||||
list: succeed(new ListBreakpointsCommand()),
|
||||
};
|
||||
}
|
60
src/cmd_line/commands/bufferDelete.ts
Normal file
60
src/cmd_line/commands/bufferDelete.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { alt, optWhitespace, Parser, seq, whitespace } from 'parsimmon';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import * as error from '../../error';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { bangParser, fileNameParser, numberParser } from '../../vimscript/parserUtils';
|
||||
|
||||
interface IBufferDeleteCommandArguments {
|
||||
bang: boolean;
|
||||
buffers: Array<string | number>;
|
||||
}
|
||||
|
||||
//
|
||||
// Implements :bd
|
||||
// http://vimdoc.sourceforge.net/htmldoc/windows.html#buffers
|
||||
//
|
||||
export class BufferDeleteCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<BufferDeleteCommand> = seq(
|
||||
bangParser.skip(optWhitespace),
|
||||
alt<string | number>(numberParser, fileNameParser).sepBy(whitespace),
|
||||
).map(([bang, buffers]) => new BufferDeleteCommand({ bang, buffers }));
|
||||
|
||||
public readonly arguments: IBufferDeleteCommandArguments;
|
||||
constructor(args: IBufferDeleteCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
if (vimState.document.isDirty && !this.arguments.bang) {
|
||||
throw error.VimError.fromCode(error.ErrorCode.NoWriteSinceLastChange);
|
||||
}
|
||||
|
||||
if (this.arguments.buffers.length === 0) {
|
||||
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
} else {
|
||||
for (const buffer of this.arguments.buffers) {
|
||||
if (typeof buffer === 'string') {
|
||||
// TODO
|
||||
StatusBar.setText(
|
||||
vimState,
|
||||
':bd[elete][!] {bufname} is not yet implemented (PRs are welcome!)',
|
||||
true,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await vscode.commands.executeCommand(`workbench.action.openEditorAtIndex${buffer}`);
|
||||
} catch (e) {
|
||||
throw error.VimError.fromCode(error.ErrorCode.NoBuffersDeleted);
|
||||
}
|
||||
|
||||
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
src/cmd_line/commands/close.ts
Normal file
43
src/cmd_line/commands/close.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Parser } from 'parsimmon';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import * as error from '../../error';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { bangParser } from '../../vimscript/parserUtils';
|
||||
|
||||
//
|
||||
// Implements :close
|
||||
// http://vimdoc.sourceforge.net/htmldoc/windows.html#:close
|
||||
//
|
||||
export class CloseCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<CloseCommand> = bangParser.map(
|
||||
(bang) => new CloseCommand(bang),
|
||||
);
|
||||
|
||||
public readonly bang: boolean;
|
||||
constructor(bang: boolean) {
|
||||
super();
|
||||
this.bang = bang;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
if (vimState.document.isDirty && !this.bang) {
|
||||
throw error.VimError.fromCode(error.ErrorCode.NoWriteSinceLastChange);
|
||||
}
|
||||
|
||||
if (vscode.window.visibleTextEditors.length === 1) {
|
||||
throw error.VimError.fromCode(error.ErrorCode.CannotCloseLastWindow);
|
||||
}
|
||||
|
||||
const oldViewColumn = vimState.editor.viewColumn;
|
||||
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
|
||||
if (
|
||||
vscode.window.activeTextEditor !== undefined &&
|
||||
vscode.window.activeTextEditor.viewColumn === oldViewColumn
|
||||
) {
|
||||
await vscode.commands.executeCommand('workbench.action.previousEditor');
|
||||
}
|
||||
}
|
||||
}
|
72
src/cmd_line/commands/copy.ts
Normal file
72
src/cmd_line/commands/copy.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { optWhitespace, Parser } from 'parsimmon';
|
||||
import { Position, Range } from 'vscode';
|
||||
import { PositionDiff } from '../../common/motion/position';
|
||||
import { ErrorCode, VimError } from '../../error';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { Address, LineRange } from '../../vimscript/lineRange';
|
||||
|
||||
export class CopyCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<CopyCommand> = optWhitespace
|
||||
.then(Address.parser.fallback(undefined))
|
||||
.map((address) => new CopyCommand(address));
|
||||
|
||||
private address?: Address;
|
||||
constructor(address?: Address) {
|
||||
super();
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public override neovimCapable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
private copyLines(vimState: VimState, sourceStart: number, sourceEnd: number) {
|
||||
const dest = this.address?.resolve(vimState, 'left', false);
|
||||
if (dest === undefined || dest < -1 || dest > vimState.document.lineCount) {
|
||||
StatusBar.displayError(vimState, VimError.fromCode(ErrorCode.InvalidAddress));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceEnd < sourceStart) {
|
||||
[sourceStart, sourceEnd] = [sourceEnd, sourceStart];
|
||||
}
|
||||
|
||||
const copiedText = vimState.document.getText(
|
||||
new Range(new Position(sourceStart, 0), new Position(sourceEnd, 0).getLineEnd()),
|
||||
);
|
||||
|
||||
let text: string;
|
||||
let position: Position;
|
||||
if (dest === -1) {
|
||||
text = copiedText + '\n';
|
||||
position = new Position(0, 0);
|
||||
} else {
|
||||
text = '\n' + copiedText;
|
||||
position = new Position(dest, 0).getLineEnd();
|
||||
}
|
||||
|
||||
const lines = copiedText.split('\n');
|
||||
const cursorPosition = new Position(
|
||||
Math.max(dest + lines.length, 0),
|
||||
lines[lines.length - 1].match(/\S/)?.index ?? 0,
|
||||
);
|
||||
|
||||
vimState.recordedState.transformer.insert(
|
||||
position,
|
||||
text,
|
||||
PositionDiff.exactPosition(cursorPosition),
|
||||
);
|
||||
}
|
||||
|
||||
public async execute(vimState: VimState): Promise<void> {
|
||||
const line = vimState.cursors[0].stop.line;
|
||||
this.copyLines(vimState, line, line);
|
||||
}
|
||||
|
||||
public override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
const { start, end } = range.resolve(vimState);
|
||||
this.copyLines(vimState, start, end);
|
||||
}
|
||||
}
|
107
src/cmd_line/commands/delete.ts
Normal file
107
src/cmd_line/commands/delete.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { Parser, alt, any, optWhitespace, seq, whitespace } from 'parsimmon';
|
||||
import { Position } from 'vscode';
|
||||
import { Register, RegisterMode } from '../../register/register';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { LineRange } from '../../vimscript/lineRange';
|
||||
import { numberParser } from '../../vimscript/parserUtils';
|
||||
|
||||
export interface IDeleteCommandArguments {
|
||||
register?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export class DeleteCommand extends ExCommand {
|
||||
// TODO: this is copy-pasted from `:y[ank]`
|
||||
public static readonly argParser: Parser<DeleteCommand> = optWhitespace.then(
|
||||
alt(
|
||||
numberParser.map((count) => {
|
||||
return { register: undefined, count };
|
||||
}),
|
||||
// eslint-disable-next-line id-denylist
|
||||
seq(any.fallback(undefined), whitespace.then(numberParser).fallback(undefined)).map(
|
||||
([register, count]) => {
|
||||
return { register, count };
|
||||
},
|
||||
),
|
||||
).map(
|
||||
({ register, count }) =>
|
||||
new DeleteCommand({
|
||||
register,
|
||||
count,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
private readonly arguments: IDeleteCommandArguments;
|
||||
constructor(args: IDeleteCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
public override neovimCapable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes text between `startLine` and `endLine`, inclusive.
|
||||
* Puts the cursor at the start of the line where the deleted range was.
|
||||
*/
|
||||
private deleteRange(startLine: number, endLine: number, vimState: VimState): void {
|
||||
let start = new Position(startLine, 0);
|
||||
let end = new Position(endLine, 0).getLineEndIncludingEOL();
|
||||
|
||||
if (endLine < vimState.document.lineCount - 1) {
|
||||
end = end.getRightThroughLineBreaks();
|
||||
} else if (startLine > 0) {
|
||||
start = start.getLeftThroughLineBreaks();
|
||||
}
|
||||
|
||||
const range = new vscode.Range(start, end);
|
||||
const text = vimState.document
|
||||
.getText(range)
|
||||
// Remove leading or trailing newline
|
||||
.replace(/^\r?\n/, '')
|
||||
.replace(/\r?\n$/, '');
|
||||
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'deleteRange',
|
||||
range: new vscode.Range(start, end),
|
||||
manuallySetCursorPositions: true,
|
||||
});
|
||||
vimState.cursorStopPosition = start.getLineBegin();
|
||||
|
||||
if (this.arguments.register) {
|
||||
vimState.recordedState.registerName = this.arguments.register;
|
||||
}
|
||||
vimState.currentRegisterMode = RegisterMode.LineWise;
|
||||
Register.put(vimState, text, 0, true);
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
const linesToRemove = this.arguments.count ?? 1;
|
||||
// :d[elete][cnt] removes [cnt] lines
|
||||
const startLine = vimState.cursorStartPosition.line;
|
||||
const endLine = startLine + (linesToRemove - 1);
|
||||
this.deleteRange(startLine, endLine, vimState);
|
||||
}
|
||||
|
||||
override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
/**
|
||||
* If a [cnt] and [range] is specified (e.g. :.+2d3), :delete [cnt] is called from
|
||||
* the end of the [range].
|
||||
* Ex. if two lines are VisualLine highlighted, :<,>d3 will :d3
|
||||
* from the end of the selected lines.
|
||||
*/
|
||||
const { start, end } = range.resolve(vimState);
|
||||
if (this.arguments.count) {
|
||||
vimState.cursorStartPosition = new Position(end, 0);
|
||||
await this.execute(vimState);
|
||||
return;
|
||||
}
|
||||
this.deleteRange(start, end, vimState);
|
||||
}
|
||||
}
|
65
src/cmd_line/commands/digraph.ts
Normal file
65
src/cmd_line/commands/digraph.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { any, Parser, seq, whitespace } from 'parsimmon';
|
||||
import { DefaultDigraphs } from '../../actions/commands/digraphs';
|
||||
import { Digraph } from '../../configuration/iconfiguration';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { TextEditor } from '../../textEditor';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { bangParser, numberParser } from '../../vimscript/parserUtils';
|
||||
import { configuration } from './../../configuration/configuration';
|
||||
|
||||
export interface IDigraphsCommandArguments {
|
||||
bang: boolean;
|
||||
newDigraph: [string, string, number[]] | undefined;
|
||||
}
|
||||
|
||||
interface DigraphQuickPickItem extends vscode.QuickPickItem {
|
||||
charCodes: number[];
|
||||
}
|
||||
|
||||
export class DigraphsCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<DigraphsCommand> = seq(
|
||||
bangParser,
|
||||
whitespace.then(seq(any, any, whitespace.then(numberParser).atLeast(1))).fallback(undefined),
|
||||
).map(([bang, newDigraph]) => new DigraphsCommand({ bang, newDigraph }));
|
||||
|
||||
private readonly arguments: IDigraphsCommandArguments;
|
||||
constructor(args: IDigraphsCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
private makeQuickPicks(digraphs: Array<[string, Digraph]>): DigraphQuickPickItem[] {
|
||||
return digraphs.map(([shortcut, [charDesc, charCodes]]) => {
|
||||
if (!Array.isArray(charCodes)) {
|
||||
charCodes = [charCodes];
|
||||
}
|
||||
return {
|
||||
label: shortcut,
|
||||
description: `${charDesc} (user)`,
|
||||
charCodes,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
if (this.arguments.newDigraph) {
|
||||
const digraph = this.arguments.newDigraph[0] + this.arguments.newDigraph[1];
|
||||
const charCodes = this.arguments.newDigraph[2];
|
||||
DefaultDigraphs.set(digraph, [String.fromCharCode(...charCodes), charCodes]);
|
||||
} else {
|
||||
const digraphKeyAndContent = this.makeQuickPicks(
|
||||
Object.entries(configuration.digraphs),
|
||||
).concat(this.makeQuickPicks([...DefaultDigraphs.entries()]));
|
||||
|
||||
void vscode.window.showQuickPick(digraphKeyAndContent).then(async (val) => {
|
||||
if (val) {
|
||||
const char = String.fromCharCode(...val.charCodes);
|
||||
await TextEditor.insert(vimState.editor, char);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
250
src/cmd_line/commands/file.ts
Normal file
250
src/cmd_line/commands/file.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Logger } from '../../util/logger';
|
||||
import { getPathDetails, resolveUri } from '../../util/path';
|
||||
import { doesFileExist } from 'platform/fs';
|
||||
import untildify = require('untildify');
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import {
|
||||
bangParser,
|
||||
FileCmd,
|
||||
fileCmdParser,
|
||||
fileNameParser,
|
||||
FileOpt,
|
||||
fileOptParser,
|
||||
} from '../../vimscript/parserUtils';
|
||||
import { optWhitespace, regexp, seq } from 'parsimmon';
|
||||
|
||||
export enum FilePosition {
|
||||
NewWindowVerticalSplit,
|
||||
NewWindowHorizontalSplit,
|
||||
}
|
||||
|
||||
export type IFileCommandArguments =
|
||||
| {
|
||||
name: 'edit';
|
||||
bang: boolean;
|
||||
opt: FileOpt;
|
||||
cmd?: FileCmd;
|
||||
file?: string;
|
||||
createFileIfNotExists?: boolean;
|
||||
}
|
||||
| {
|
||||
name: 'enew';
|
||||
bang: boolean;
|
||||
createFileIfNotExists?: boolean;
|
||||
}
|
||||
| {
|
||||
name: 'new' | 'vnew' | 'split' | 'vsplit';
|
||||
opt: FileOpt;
|
||||
cmd?: FileCmd;
|
||||
file?: string;
|
||||
createFileIfNotExists?: boolean;
|
||||
};
|
||||
|
||||
// TODO: This is a hack to get this to work in the short term with new arg parsing logic.
|
||||
type LegacyArgs = {
|
||||
file?: string;
|
||||
bang?: boolean;
|
||||
position?: FilePosition;
|
||||
cmd?: FileCmd;
|
||||
createFileIfNotExists?: boolean;
|
||||
};
|
||||
function getLegacyArgs(args: IFileCommandArguments): LegacyArgs {
|
||||
if (args.name === 'edit') {
|
||||
return { file: args.file, bang: args.bang, cmd: args.cmd, createFileIfNotExists: true };
|
||||
} else if (args.name === 'enew') {
|
||||
return { bang: args.bang, createFileIfNotExists: true };
|
||||
} else if (args.name === 'new') {
|
||||
return {
|
||||
file: args.file,
|
||||
position: FilePosition.NewWindowHorizontalSplit,
|
||||
createFileIfNotExists: true,
|
||||
};
|
||||
} else if (args.name === 'vnew') {
|
||||
return {
|
||||
file: args.file,
|
||||
position: FilePosition.NewWindowVerticalSplit,
|
||||
createFileIfNotExists: true,
|
||||
};
|
||||
} else if (args.name === 'split') {
|
||||
return {
|
||||
file: args.file,
|
||||
position: FilePosition.NewWindowHorizontalSplit,
|
||||
// only to create if file arg is specified
|
||||
createFileIfNotExists: args.file !== undefined,
|
||||
};
|
||||
} else if (args.name === 'vsplit') {
|
||||
return {
|
||||
file: args.file,
|
||||
position: FilePosition.NewWindowVerticalSplit,
|
||||
// only to create if file arg is specified
|
||||
createFileIfNotExists: args.file !== undefined,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unexpected FileCommand.arguments.name: ${args.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class FileCommand extends ExCommand {
|
||||
// TODO: There's a lot of duplication here
|
||||
// TODO: These `optWhitespace` calls should be `whitespace`
|
||||
public static readonly argParsers = {
|
||||
edit: seq(
|
||||
bangParser,
|
||||
optWhitespace.then(fileOptParser).fallback([]),
|
||||
optWhitespace.then(fileCmdParser).fallback(undefined),
|
||||
optWhitespace.then(fileNameParser).fallback(undefined),
|
||||
).map(([bang, opt, cmd, file]) => new FileCommand({ name: 'edit', bang, opt, cmd, file })),
|
||||
enew: bangParser.map((bang) => new FileCommand({ name: 'enew', bang })),
|
||||
new: seq(
|
||||
optWhitespace.then(fileOptParser).fallback([]),
|
||||
optWhitespace.then(fileCmdParser).fallback(undefined),
|
||||
optWhitespace.then(fileNameParser).fallback(undefined),
|
||||
).map(([opt, cmd, file]) => new FileCommand({ name: 'new', opt, cmd, file })),
|
||||
split: seq(
|
||||
optWhitespace.then(fileOptParser).fallback([]),
|
||||
optWhitespace.then(fileCmdParser).fallback(undefined),
|
||||
optWhitespace.then(fileNameParser).fallback(undefined),
|
||||
).map(([opt, cmd, file]) => new FileCommand({ name: 'split', opt, cmd, file })),
|
||||
vnew: seq(
|
||||
optWhitespace.then(fileOptParser).fallback([]),
|
||||
optWhitespace.then(fileCmdParser).fallback(undefined),
|
||||
optWhitespace.then(fileNameParser).fallback(undefined),
|
||||
).map(([opt, cmd, file]) => new FileCommand({ name: 'vnew', opt, cmd, file })),
|
||||
vsplit: seq(
|
||||
optWhitespace.then(fileOptParser).fallback([]),
|
||||
optWhitespace.then(fileCmdParser).fallback(undefined),
|
||||
optWhitespace.then(fileNameParser).fallback(undefined),
|
||||
).map(([opt, cmd, file]) => new FileCommand({ name: 'vsplit', opt, cmd, file })),
|
||||
};
|
||||
|
||||
private readonly arguments: IFileCommandArguments;
|
||||
|
||||
constructor(args: IFileCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
const args = getLegacyArgs(this.arguments);
|
||||
|
||||
if (args.bang) {
|
||||
await vscode.commands.executeCommand('workbench.action.files.revert');
|
||||
return;
|
||||
}
|
||||
|
||||
// Need to do this before the split since it loses the activeTextEditor
|
||||
const editorFileUri = vscode.window.activeTextEditor!.document.uri;
|
||||
const editorFilePath = editorFileUri.fsPath;
|
||||
|
||||
// Do the split if requested
|
||||
let split = false;
|
||||
if (args.position === FilePosition.NewWindowVerticalSplit) {
|
||||
await vscode.commands.executeCommand('workbench.action.splitEditorRight');
|
||||
split = true;
|
||||
}
|
||||
if (args.position === FilePosition.NewWindowHorizontalSplit) {
|
||||
await vscode.commands.executeCommand('workbench.action.splitEditorDown');
|
||||
split = true;
|
||||
}
|
||||
|
||||
const hidePreviousEditor = async () => {
|
||||
if (split === true) {
|
||||
await vscode.commands.executeCommand('workbench.action.previousEditor');
|
||||
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
}
|
||||
};
|
||||
|
||||
// No file was specified
|
||||
if (args.file === undefined) {
|
||||
if (args.createFileIfNotExists === true) {
|
||||
await vscode.commands.executeCommand('workbench.action.files.newUntitledFile');
|
||||
await hidePreviousEditor();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Only untidify when the currently open page and file completion is local
|
||||
if (args.file && editorFileUri.scheme === 'file') {
|
||||
args.file = untildify(args.file);
|
||||
}
|
||||
|
||||
let fileUri = editorFileUri;
|
||||
// Using the empty string will request to open a file
|
||||
if (args.file === '') {
|
||||
// No file on split is fine and just return
|
||||
if (split === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileList = await vscode.window.showOpenDialog({});
|
||||
if (fileList && fileList.length > 0) {
|
||||
fileUri = fileList[0];
|
||||
}
|
||||
} else {
|
||||
// remove file://
|
||||
args.file = args.file.replace(/^file:\/\//, '');
|
||||
|
||||
// Using a filename, open or create the file
|
||||
const isRemote = !!vscode.env.remoteName;
|
||||
const { fullPath, path: p } = getPathDetails(args.file, editorFileUri, isRemote);
|
||||
// Only if the expanded path of the full path is different than
|
||||
// the currently opened window path
|
||||
if (fullPath !== editorFilePath) {
|
||||
const uriPath = resolveUri(fullPath, p.sep, editorFileUri, isRemote);
|
||||
if (uriPath === null) {
|
||||
// return if the path is invalid
|
||||
return;
|
||||
}
|
||||
|
||||
let fileExists = await doesFileExist(uriPath);
|
||||
if (fileExists) {
|
||||
// If the file without the added ext exists
|
||||
fileUri = uriPath;
|
||||
} else {
|
||||
// if file does not exist
|
||||
// try to find it with the same extension as the current file
|
||||
const pathWithExt = fullPath + p.extname(editorFilePath);
|
||||
const uriPathWithExt = resolveUri(pathWithExt, p.sep, editorFileUri, isRemote);
|
||||
if (uriPathWithExt !== null) {
|
||||
fileExists = await doesFileExist(uriPathWithExt);
|
||||
if (fileExists) {
|
||||
// if the file with the added ext exists
|
||||
fileUri = uriPathWithExt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If both with and without ext path do not exist
|
||||
if (!fileExists) {
|
||||
if (args.createFileIfNotExists) {
|
||||
// Change the scheme to untitled to open an
|
||||
// untitled tab
|
||||
fileUri = uriPath.with({ scheme: 'untitled' });
|
||||
} else {
|
||||
Logger.error(`${args.file} does not exist.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(fileUri);
|
||||
const editor = await vscode.window.showTextDocument(doc);
|
||||
|
||||
const lineNumber =
|
||||
args.cmd?.type === 'line_number'
|
||||
? args.cmd.line
|
||||
: args.cmd?.type === 'last_line'
|
||||
? vscode.window.activeTextEditor!.document.lineCount - 1
|
||||
: undefined;
|
||||
if (lineNumber !== undefined && lineNumber >= 0) {
|
||||
const pos = new vscode.Position(lineNumber, 0);
|
||||
editor.selection = new vscode.Selection(pos, pos);
|
||||
const range = new vscode.Range(pos, pos);
|
||||
editor.revealRange(range);
|
||||
}
|
||||
await hidePreviousEditor();
|
||||
}
|
||||
}
|
26
src/cmd_line/commands/fileInfo.ts
Normal file
26
src/cmd_line/commands/fileInfo.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { all, optWhitespace, Parser, seq } from 'parsimmon';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { reportFileInfo } from '../../util/statusBarTextUtils';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { bangParser } from '../../vimscript/parserUtils';
|
||||
|
||||
export class FileInfoCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<FileInfoCommand> = seq(
|
||||
bangParser,
|
||||
optWhitespace.then(all),
|
||||
).map(([bang, fileName]) => new FileInfoCommand({ bang, fileName }));
|
||||
|
||||
private args: {
|
||||
bang: boolean;
|
||||
fileName?: string;
|
||||
};
|
||||
private constructor(args: { bang: boolean; fileName?: string }) {
|
||||
super();
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
// TODO: Use `this.args`
|
||||
reportFileInfo(vimState.cursors[0].start, vimState);
|
||||
}
|
||||
}
|
32
src/cmd_line/commands/goto.ts
Normal file
32
src/cmd_line/commands/goto.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { optWhitespace, Parser } from 'parsimmon';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { LineRange } from '../../vimscript/lineRange';
|
||||
import { numberParser } from '../../vimscript/parserUtils';
|
||||
|
||||
export class GotoCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<GotoCommand> = optWhitespace
|
||||
.then(numberParser.fallback(undefined))
|
||||
.map((count) => new GotoCommand(count));
|
||||
|
||||
private offset?: number;
|
||||
constructor(offset?: number) {
|
||||
super();
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
private gotoOffset(vimState: VimState, offset: number) {
|
||||
vimState.cursorStopPosition = vimState.document.positionAt(offset);
|
||||
}
|
||||
|
||||
public async execute(vimState: VimState): Promise<void> {
|
||||
this.gotoOffset(vimState, this.offset ?? 0);
|
||||
}
|
||||
|
||||
public override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
if (this.offset === undefined) {
|
||||
this.offset = range.resolve(vimState)?.end ?? 0;
|
||||
}
|
||||
this.gotoOffset(vimState, this.offset);
|
||||
}
|
||||
}
|
15
src/cmd_line/commands/gotoLine.ts
Normal file
15
src/cmd_line/commands/gotoLine.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { LineRange } from '../../vimscript/lineRange';
|
||||
|
||||
export class GotoLineCommand extends ExCommand {
|
||||
public async execute(vimState: VimState): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
public override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
vimState.cursorStartPosition = vimState.cursorStopPosition = vimState.cursorStopPosition
|
||||
.with({ line: range.resolve(vimState).end })
|
||||
.obeyStartOfLine(vimState.document);
|
||||
}
|
||||
}
|
69
src/cmd_line/commands/history.ts
Normal file
69
src/cmd_line/commands/history.ts
Normal file
@ -0,0 +1,69 @@
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { Parser, alt, optWhitespace, string } from 'parsimmon';
|
||||
import {
|
||||
CommandShowCommandHistory,
|
||||
CommandShowSearchHistory,
|
||||
} from '../../actions/commands/actions';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { nameAbbrevParser } from '../../vimscript/parserUtils';
|
||||
import { SearchDirection } from '../../vimscript/pattern';
|
||||
|
||||
export enum HistoryCommandType {
|
||||
Cmd,
|
||||
Search,
|
||||
Expr,
|
||||
Input,
|
||||
Debug,
|
||||
All,
|
||||
}
|
||||
|
||||
const historyTypeParser: Parser<HistoryCommandType> = alt(
|
||||
alt(nameAbbrevParser('c', 'md'), string(':')).result(HistoryCommandType.Cmd),
|
||||
alt(nameAbbrevParser('s', 'earch'), string('/')).result(HistoryCommandType.Search),
|
||||
alt(nameAbbrevParser('e', 'xpr'), string('=')).result(HistoryCommandType.Expr),
|
||||
alt(nameAbbrevParser('i', 'nput'), string('@')).result(HistoryCommandType.Input),
|
||||
alt(nameAbbrevParser('d', 'ebug'), string('>')).result(HistoryCommandType.Debug),
|
||||
nameAbbrevParser('a', 'll').result(HistoryCommandType.All),
|
||||
);
|
||||
|
||||
export interface IHistoryCommandArguments {
|
||||
type: HistoryCommandType;
|
||||
// TODO: :history can also accept a range
|
||||
}
|
||||
|
||||
// http://vimdoc.sourceforge.net/htmldoc/cmdline.html#:history
|
||||
export class HistoryCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<HistoryCommand> = optWhitespace
|
||||
.then(historyTypeParser.fallback(HistoryCommandType.Cmd))
|
||||
.map((type) => new HistoryCommand({ type }));
|
||||
|
||||
private readonly arguments: IHistoryCommandArguments;
|
||||
constructor(args: IHistoryCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
switch (this.arguments.type) {
|
||||
case HistoryCommandType.Cmd:
|
||||
await new CommandShowCommandHistory().exec(vimState.cursorStopPosition, vimState);
|
||||
break;
|
||||
case HistoryCommandType.Search:
|
||||
await new CommandShowSearchHistory(SearchDirection.Forward).exec(
|
||||
vimState.cursorStopPosition,
|
||||
vimState,
|
||||
);
|
||||
break;
|
||||
// TODO: Implement these
|
||||
case HistoryCommandType.Expr:
|
||||
throw new Error('Not implemented');
|
||||
case HistoryCommandType.Input:
|
||||
throw new Error('Not implemented');
|
||||
case HistoryCommandType.Debug:
|
||||
throw new Error('Not implemented');
|
||||
case HistoryCommandType.All:
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
}
|
53
src/cmd_line/commands/jumps.ts
Normal file
53
src/cmd_line/commands/jumps.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { QuickPickItem, window } from 'vscode';
|
||||
|
||||
import { Cursor } from '../../common/motion/cursor';
|
||||
import { Jump } from '../../jumps/jump';
|
||||
import { globalState } from '../../state/globalState';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
|
||||
class JumpPickItem implements QuickPickItem {
|
||||
jump: Jump;
|
||||
|
||||
label: string;
|
||||
description?: string;
|
||||
detail?: string;
|
||||
picked?: boolean;
|
||||
alwaysShow?: boolean;
|
||||
|
||||
constructor(jump: Jump, idx: number) {
|
||||
this.jump = jump;
|
||||
this.label = jump.fileName;
|
||||
this.detail = `jump ${idx} line ${jump.position.line + 1} col ${jump.position.character}`;
|
||||
try {
|
||||
this.description = jump.document.lineAt(jump.position).text;
|
||||
} catch (e) {
|
||||
this.description = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class JumpsCommand extends ExCommand {
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
const jumpTracker = globalState.jumpTracker;
|
||||
if (jumpTracker.hasJumps) {
|
||||
const quickPickItems = jumpTracker.jumps.map((jump, idx) => new JumpPickItem(jump, idx));
|
||||
const item = await window.showQuickPick(quickPickItems, {
|
||||
canPickMany: false,
|
||||
});
|
||||
if (item && item.jump.document !== undefined) {
|
||||
void window.showTextDocument(item.jump.document);
|
||||
vimState.cursors = [new Cursor(item.jump.position, item.jump.position)];
|
||||
}
|
||||
} else {
|
||||
void window.showInformationMessage('No jumps available');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ClearJumpsCommand extends ExCommand {
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
const jumpTracker = globalState.jumpTracker;
|
||||
jumpTracker.clearJumps();
|
||||
}
|
||||
}
|
134
src/cmd_line/commands/leftRightCenter.ts
Normal file
134
src/cmd_line/commands/leftRightCenter.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { optWhitespace, Parser } from 'parsimmon';
|
||||
import { Range, TextLine } from 'vscode';
|
||||
import { configuration } from '../../configuration/configuration';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { Address, LineRange } from '../../vimscript/lineRange';
|
||||
import { numberParser } from '../../vimscript/parserUtils';
|
||||
|
||||
type LeftArgs = {
|
||||
indent: number;
|
||||
};
|
||||
|
||||
export class LeftCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<LeftCommand> = optWhitespace
|
||||
.then(numberParser.fallback(0))
|
||||
.map((indent) => new LeftCommand({ indent }));
|
||||
|
||||
private args: LeftArgs;
|
||||
constructor(args: LeftArgs) {
|
||||
super();
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
void this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' })));
|
||||
}
|
||||
|
||||
override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
const { start, end } = range.resolve(vimState);
|
||||
|
||||
const lines: TextLine[] = [];
|
||||
for (let line = start; line <= end; line++) {
|
||||
lines.push(vimState.document.lineAt(line));
|
||||
}
|
||||
|
||||
vimState.recordedState.transformer.replace(
|
||||
new Range(lines[0].range.start, lines[lines.length - 1].range.end),
|
||||
lines
|
||||
.map(
|
||||
(line) =>
|
||||
' '.repeat(this.args.indent) + line.text.slice(line.firstNonWhitespaceCharacterIndex),
|
||||
)
|
||||
.join('\n'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type RightArgs = {
|
||||
width: number;
|
||||
};
|
||||
|
||||
export class RightCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<RightCommand> = optWhitespace
|
||||
.then(numberParser.fallback(undefined))
|
||||
.map((width) => new RightCommand({ width: width ?? configuration.textwidth }));
|
||||
|
||||
private args: RightArgs;
|
||||
constructor(args: RightArgs) {
|
||||
super();
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
void this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' })));
|
||||
}
|
||||
|
||||
override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
const { start, end } = range.resolve(vimState);
|
||||
|
||||
const lines: TextLine[] = [];
|
||||
for (let line = start; line <= end; line++) {
|
||||
lines.push(vimState.document.lineAt(line));
|
||||
}
|
||||
|
||||
vimState.recordedState.transformer.replace(
|
||||
new Range(lines[0].range.start, lines[lines.length - 1].range.end),
|
||||
lines
|
||||
.map((line) => {
|
||||
const indent = ' '.repeat(
|
||||
Math.max(
|
||||
0,
|
||||
this.args.width - (line.text.length - line.firstNonWhitespaceCharacterIndex),
|
||||
),
|
||||
);
|
||||
return indent + line.text.slice(line.firstNonWhitespaceCharacterIndex);
|
||||
})
|
||||
.join('\n'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type CenterArgs = {
|
||||
width: number;
|
||||
};
|
||||
|
||||
export class CenterCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<CenterCommand> = optWhitespace
|
||||
.then(numberParser.fallback(undefined))
|
||||
.map((width) => new CenterCommand({ width: width ?? configuration.textwidth }));
|
||||
|
||||
private args: CenterArgs;
|
||||
constructor(args: CenterArgs) {
|
||||
super();
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
void this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' })));
|
||||
}
|
||||
|
||||
override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
const { start, end } = range.resolve(vimState);
|
||||
|
||||
const lines: TextLine[] = [];
|
||||
for (let line = start; line <= end; line++) {
|
||||
lines.push(vimState.document.lineAt(line));
|
||||
}
|
||||
|
||||
vimState.recordedState.transformer.replace(
|
||||
new Range(lines[0].range.start, lines[lines.length - 1].range.end),
|
||||
lines
|
||||
.map((line) => {
|
||||
const indent = ' '.repeat(
|
||||
Math.max(
|
||||
0,
|
||||
this.args.width - (line.text.length - line.firstNonWhitespaceCharacterIndex),
|
||||
) / 2,
|
||||
);
|
||||
return indent + line.text.slice(line.firstNonWhitespaceCharacterIndex);
|
||||
})
|
||||
.join('\n'),
|
||||
);
|
||||
}
|
||||
}
|
135
src/cmd_line/commands/marks.ts
Normal file
135
src/cmd_line/commands/marks.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { QuickPickItem, window } from 'vscode';
|
||||
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { Parser, alt, noneOf, optWhitespace, regexp, seq, string, whitespace } from 'parsimmon';
|
||||
import { Cursor } from '../../common/motion/cursor';
|
||||
import { ErrorCode, VimError } from '../../error';
|
||||
import { IMark } from '../../history/historyTracker';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
|
||||
class MarkQuickPickItem implements QuickPickItem {
|
||||
mark: IMark;
|
||||
|
||||
label: string;
|
||||
description: string;
|
||||
detail: string;
|
||||
picked = false;
|
||||
alwaysShow = false;
|
||||
|
||||
constructor(vimState: VimState, mark: IMark) {
|
||||
this.mark = mark;
|
||||
this.label = mark.name;
|
||||
if (mark.document && mark.document !== vimState.document) {
|
||||
this.description = mark.document.fileName;
|
||||
} else {
|
||||
this.description = vimState.document.lineAt(mark.position).text.trim();
|
||||
}
|
||||
this.detail = `line ${mark.position.line} col ${mark.position.character}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class MarksCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<MarksCommand> = optWhitespace
|
||||
.then(noneOf('|'))
|
||||
.many()
|
||||
.map((marks) => new MarksCommand(marks));
|
||||
|
||||
private marksFilter: string[];
|
||||
constructor(marksFilter: string[]) {
|
||||
super();
|
||||
this.marksFilter = marksFilter;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
const quickPickItems: MarkQuickPickItem[] = vimState.historyTracker
|
||||
.getMarks()
|
||||
.filter((mark) => {
|
||||
return this.marksFilter.length === 0 || this.marksFilter.includes(mark.name);
|
||||
})
|
||||
.map((mark) => new MarkQuickPickItem(vimState, mark));
|
||||
|
||||
if (quickPickItems.length > 0) {
|
||||
const item = await window.showQuickPick(quickPickItems, {
|
||||
canPickMany: false,
|
||||
});
|
||||
if (item) {
|
||||
vimState.cursors = [new Cursor(item.mark.position, item.mark.position)];
|
||||
}
|
||||
} else {
|
||||
void window.showInformationMessage('No marks set');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DeleteMarksArgs = Array<{ start: string; end: string } | string> | '!';
|
||||
|
||||
export class DeleteMarksCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<DeleteMarksCommand> = alt<DeleteMarksArgs>(
|
||||
string('!'),
|
||||
whitespace.then(
|
||||
optWhitespace
|
||||
.then(
|
||||
alt<{ start: string; end: string } | string>(
|
||||
seq(regexp(/[a-z]/).skip(string('-')), regexp(/[a-z]/)).map(([start, end]) => {
|
||||
return { start, end };
|
||||
}),
|
||||
seq(regexp(/[A-Z]/).skip(string('-')), regexp(/[A-Z]/)).map(([start, end]) => {
|
||||
return { start, end };
|
||||
}),
|
||||
seq(regexp(/[0-9]/).skip(string('-')), regexp(/[0-9]/)).map(([start, end]) => {
|
||||
return { start, end };
|
||||
}),
|
||||
noneOf('-'),
|
||||
),
|
||||
)
|
||||
.many(),
|
||||
),
|
||||
).map((marks) => new DeleteMarksCommand(marks));
|
||||
|
||||
private args: DeleteMarksArgs;
|
||||
constructor(args: DeleteMarksArgs) {
|
||||
super();
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
private static resolveMarkList(vimState: VimState, args: DeleteMarksArgs) {
|
||||
const asciiRange = (start: string, end: string) => {
|
||||
if (start > end) {
|
||||
throw VimError.fromCode(ErrorCode.InvalidArgument);
|
||||
}
|
||||
|
||||
const [asciiStart, asciiEnd] = [start.charCodeAt(0), end.charCodeAt(0)];
|
||||
|
||||
const chars: string[] = [];
|
||||
for (let ascii = asciiStart; ascii <= asciiEnd; ascii++) {
|
||||
chars.push(String.fromCharCode(ascii));
|
||||
}
|
||||
return chars;
|
||||
};
|
||||
|
||||
if (args === '!') {
|
||||
// TODO: clear change list
|
||||
return asciiRange('a', 'z');
|
||||
}
|
||||
|
||||
const marks: string[] = [];
|
||||
for (const x of args) {
|
||||
if (typeof x === 'string') {
|
||||
marks.push(x);
|
||||
} else {
|
||||
const range = asciiRange(x.start, x.end);
|
||||
if (range === undefined) {
|
||||
throw VimError.fromCode(ErrorCode.InvalidArgument);
|
||||
}
|
||||
marks.push(...range.concat());
|
||||
}
|
||||
}
|
||||
return marks;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
const marks = DeleteMarksCommand.resolveMarkList(vimState, this.args);
|
||||
vimState.historyTracker.removeMarks(marks);
|
||||
}
|
||||
}
|
105
src/cmd_line/commands/move.ts
Normal file
105
src/cmd_line/commands/move.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { optWhitespace, Parser } from 'parsimmon';
|
||||
import { Position, Range } from 'vscode';
|
||||
import { PositionDiff } from '../../common/motion/position';
|
||||
import { ErrorCode, VimError } from '../../error';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { Address, LineRange } from '../../vimscript/lineRange';
|
||||
|
||||
export class MoveCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<MoveCommand> = optWhitespace
|
||||
.then(Address.parser.fallback(undefined))
|
||||
.map((address) => new MoveCommand(address));
|
||||
|
||||
private address?: Address;
|
||||
constructor(address?: Address) {
|
||||
super();
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public override neovimCapable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
private moveLines(vimState: VimState, sourceStart: number, sourceEnd: number) {
|
||||
const dest = this.address?.resolve(vimState, 'left', false);
|
||||
if (dest === undefined || dest < -1 || dest > vimState.document.lineCount) {
|
||||
StatusBar.displayError(vimState, VimError.fromCode(ErrorCode.InvalidAddress));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceEnd < sourceStart) {
|
||||
[sourceStart, sourceEnd] = [sourceEnd, sourceStart];
|
||||
}
|
||||
/* make sure
|
||||
1. not move a range to the place inside itself.
|
||||
2. not move a range to the place right below or above itself, which leads to no change.
|
||||
*/
|
||||
if (dest >= sourceStart && dest <= sourceEnd) {
|
||||
StatusBar.displayError(vimState, VimError.fromCode(ErrorCode.InvalidAddress));
|
||||
return;
|
||||
}
|
||||
|
||||
// copy
|
||||
const copiedText = vimState.document.getText(
|
||||
new Range(new Position(sourceStart, 0), new Position(sourceEnd, 0).getLineEnd()),
|
||||
);
|
||||
|
||||
let text: string;
|
||||
let position: Position;
|
||||
if (dest === -1) {
|
||||
text = copiedText + '\n';
|
||||
position = new Position(0, 0);
|
||||
} else {
|
||||
text = '\n' + copiedText;
|
||||
position = new Position(dest, 0).getLineEnd();
|
||||
}
|
||||
|
||||
const lines = copiedText.split('\n');
|
||||
let cursorPosition: Position;
|
||||
if (dest > sourceEnd) {
|
||||
// make the cursor position at the beginning of the endline.
|
||||
cursorPosition = new Position(
|
||||
Math.max(dest, 0),
|
||||
lines[lines.length - 1].match(/\S/)?.index ?? 0,
|
||||
);
|
||||
} else {
|
||||
cursorPosition = new Position(
|
||||
Math.max(dest + lines.length, 0),
|
||||
lines[lines.length - 1].match(/\S/)?.index ?? 0,
|
||||
);
|
||||
}
|
||||
// delete
|
||||
let start = new Position(sourceStart, 0);
|
||||
let end = new Position(sourceEnd, 0).getLineEndIncludingEOL();
|
||||
|
||||
if (sourceEnd < vimState.document.lineCount - 1) {
|
||||
end = end.getRightThroughLineBreaks();
|
||||
} else if (sourceStart > 0) {
|
||||
start = start.getLeftThroughLineBreaks();
|
||||
}
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'deleteRange',
|
||||
range: new Range(start, end),
|
||||
manuallySetCursorPositions: true,
|
||||
});
|
||||
|
||||
// insert
|
||||
vimState.recordedState.transformer.insert(
|
||||
position,
|
||||
text,
|
||||
PositionDiff.exactPosition(cursorPosition),
|
||||
);
|
||||
}
|
||||
|
||||
public async execute(vimState: VimState): Promise<void> {
|
||||
const line = vimState.cursors[0].stop.line;
|
||||
this.moveLines(vimState, line, line);
|
||||
}
|
||||
|
||||
public override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
const { start, end } = range.resolve(vimState);
|
||||
this.moveLines(vimState, start, end);
|
||||
}
|
||||
}
|
13
src/cmd_line/commands/nohl.ts
Normal file
13
src/cmd_line/commands/nohl.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { globalState } from '../../state/globalState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
|
||||
export class NohlCommand extends ExCommand {
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
globalState.hl = false;
|
||||
|
||||
// Clear the `match x of y` message from status bar
|
||||
StatusBar.clear(vimState);
|
||||
}
|
||||
}
|
13
src/cmd_line/commands/only.ts
Normal file
13
src/cmd_line/commands/only.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
|
||||
export class OnlyCommand extends ExCommand {
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
await Promise.allSettled([
|
||||
vscode.commands.executeCommand('workbench.action.closeEditorsInOtherGroups'),
|
||||
vscode.commands.executeCommand('workbench.action.maximizeEditor'),
|
||||
vscode.commands.executeCommand('workbench.action.closePanel'),
|
||||
]);
|
||||
}
|
||||
}
|
54
src/cmd_line/commands/print.ts
Normal file
54
src/cmd_line/commands/print.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Parser, succeed } from 'parsimmon';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { Address, LineRange } from '../../vimscript/lineRange';
|
||||
|
||||
type PrintArgs = {
|
||||
printNumbers: boolean;
|
||||
printText: boolean;
|
||||
};
|
||||
|
||||
// TODO: `:l[ist]` is more than an alias
|
||||
// TODO: `:z`
|
||||
export class PrintCommand extends ExCommand {
|
||||
// TODO: Print {count} and [flags]
|
||||
public static readonly argParser = (args: {
|
||||
printNumbers: boolean;
|
||||
printText: boolean;
|
||||
}): Parser<PrintCommand> => succeed(new PrintCommand(args));
|
||||
|
||||
private args: PrintArgs;
|
||||
constructor(args: PrintArgs) {
|
||||
super();
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
// TODO: Wrong default for `:=`
|
||||
void this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' })));
|
||||
}
|
||||
|
||||
override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
const { end } = range.resolve(vimState);
|
||||
|
||||
// For now, we just print the last line.
|
||||
// TODO: Create a dynamic document if there's more than one line?
|
||||
const line = vimState.document.lineAt(end);
|
||||
let output: string;
|
||||
if (this.args.printNumbers) {
|
||||
if (this.args.printText) {
|
||||
output = `${line.lineNumber + 1} ${line.text}`;
|
||||
} else {
|
||||
output = `${line.lineNumber + 1}`;
|
||||
}
|
||||
} else {
|
||||
if (this.args.printText) {
|
||||
output = `${line.text}`;
|
||||
} else {
|
||||
output = '';
|
||||
}
|
||||
}
|
||||
StatusBar.setText(vimState, output);
|
||||
}
|
||||
}
|
83
src/cmd_line/commands/put.ts
Normal file
83
src/cmd_line/commands/put.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { configuration } from '../../configuration/configuration';
|
||||
import { VimState } from '../../state/vimState';
|
||||
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { Parser, alt, any, optWhitespace, seq } from 'parsimmon';
|
||||
import { Position } from 'vscode';
|
||||
import { PutBeforeFromCmdLine, PutFromCmdLine } from '../../actions/commands/put';
|
||||
import { ErrorCode, VimError } from '../../error';
|
||||
import { Register } from '../../register/register';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { LineRange } from '../../vimscript/lineRange';
|
||||
import { bangParser } from '../../vimscript/parserUtils';
|
||||
import { expressionParser } from '../expression';
|
||||
|
||||
export interface IPutCommandArguments {
|
||||
bang: boolean;
|
||||
register?: string;
|
||||
fromExpression?: string;
|
||||
}
|
||||
|
||||
//
|
||||
// Implements :put
|
||||
// http://vimdoc.sourceforge.net/htmldoc/change.html#:put
|
||||
//
|
||||
|
||||
export class PutExCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<PutExCommand> = seq(
|
||||
bangParser,
|
||||
alt(
|
||||
expressionParser,
|
||||
optWhitespace
|
||||
.then(any)
|
||||
.map((x) => ({ register: x }))
|
||||
.fallback({ register: undefined }),
|
||||
),
|
||||
).map(([bang, register]) => new PutExCommand({ bang, ...register }));
|
||||
|
||||
public readonly arguments: IPutCommandArguments;
|
||||
|
||||
constructor(args: IPutCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
public override neovimCapable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async doPut(vimState: VimState, position: Position): Promise<void> {
|
||||
if (this.arguments.fromExpression && this.arguments.register) {
|
||||
// set the register to the value of the expression
|
||||
Register.overwriteRegister(
|
||||
vimState,
|
||||
this.arguments.register,
|
||||
this.arguments.fromExpression,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
const registerName = this.arguments.register || (configuration.useSystemClipboard ? '*' : '"');
|
||||
|
||||
if (!Register.isValidRegister(registerName)) {
|
||||
StatusBar.displayError(vimState, VimError.fromCode(ErrorCode.TrailingCharacters));
|
||||
return;
|
||||
}
|
||||
|
||||
vimState.recordedState.registerName = registerName;
|
||||
|
||||
const putCmd = this.arguments.bang ? new PutBeforeFromCmdLine() : new PutFromCmdLine();
|
||||
putCmd.setInsertionLine(position.line);
|
||||
await putCmd.exec(position, vimState);
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
await this.doPut(vimState, vimState.cursorStopPosition);
|
||||
}
|
||||
|
||||
override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
const { end } = range.resolve(vimState);
|
||||
await this.doPut(vimState, new Position(end, 0).getLineEnd());
|
||||
}
|
||||
}
|
61
src/cmd_line/commands/quit.ts
Normal file
61
src/cmd_line/commands/quit.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { Parser } from 'parsimmon';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import * as error from '../../error';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { bangParser } from '../../vimscript/parserUtils';
|
||||
|
||||
export interface IQuitCommandArguments {
|
||||
bang?: boolean;
|
||||
quitAll?: boolean;
|
||||
}
|
||||
|
||||
//
|
||||
// Implements :quit
|
||||
// http://vimdoc.sourceforge.net/htmldoc/editing.html#:quit
|
||||
//
|
||||
export class QuitCommand extends ExCommand {
|
||||
public static readonly argParser: (quitAll: boolean) => Parser<QuitCommand> = (
|
||||
quitAll: boolean,
|
||||
) =>
|
||||
bangParser.map(
|
||||
(bang) =>
|
||||
new QuitCommand({
|
||||
bang,
|
||||
quitAll,
|
||||
}),
|
||||
);
|
||||
|
||||
public override isRepeatableWithDot = false;
|
||||
|
||||
public arguments: IQuitCommandArguments;
|
||||
constructor(args: IQuitCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
// NOTE: We can't currently get all open text editors, so this isn't perfect. See #3809
|
||||
const duplicatedInSplit =
|
||||
vscode.window.visibleTextEditors.filter((editor) => editor.document === vimState.document)
|
||||
.length > 1;
|
||||
if (
|
||||
vimState.document.isDirty &&
|
||||
!this.arguments.bang &&
|
||||
(!duplicatedInSplit || this.arguments.quitAll)
|
||||
) {
|
||||
throw error.VimError.fromCode(error.ErrorCode.NoWriteSinceLastChange);
|
||||
}
|
||||
|
||||
if (this.arguments.quitAll) {
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
} else {
|
||||
if (!this.arguments.bang) {
|
||||
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
} else {
|
||||
await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
87
src/cmd_line/commands/read.ts
Normal file
87
src/cmd_line/commands/read.ts
Normal file
@ -0,0 +1,87 @@
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { all, alt, optWhitespace, Parser, seq, string, whitespace } from 'parsimmon';
|
||||
import { SUPPORT_READ_COMMAND } from 'platform/constants';
|
||||
import { readFileAsync } from 'platform/fs';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { fileNameParser, FileOpt, fileOptParser } from '../../vimscript/parserUtils';
|
||||
|
||||
export type IReadCommandArguments = {
|
||||
opt: FileOpt;
|
||||
} & ({ cmd: string } | { file: string } | object);
|
||||
|
||||
//
|
||||
// Implements :read and :read!
|
||||
// http://vimdoc.sourceforge.net/htmldoc/insert.html#:read
|
||||
// http://vimdoc.sourceforge.net/htmldoc/insert.html#:read!
|
||||
//
|
||||
export class ReadCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<ReadCommand> = seq(
|
||||
whitespace.then(fileOptParser).fallback([]),
|
||||
optWhitespace
|
||||
.then(
|
||||
alt<{ cmd: string } | { file: string }>(
|
||||
string('!')
|
||||
.then(all)
|
||||
.map((cmd) => {
|
||||
return { cmd };
|
||||
}),
|
||||
fileNameParser.map((file) => {
|
||||
return { file };
|
||||
}),
|
||||
),
|
||||
)
|
||||
.fallback(undefined),
|
||||
).map(([opt, other]) => new ReadCommand({ opt, ...other }));
|
||||
|
||||
private readonly arguments: IReadCommandArguments;
|
||||
constructor(args: IReadCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
public override neovimCapable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
const textToInsert = await this.getTextToInsert(vimState);
|
||||
if (textToInsert) {
|
||||
vimState.recordedState.transformer.insert(
|
||||
vimState.cursorStopPosition.getLineEnd(),
|
||||
'\n' + textToInsert,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: executeWithRange()
|
||||
|
||||
async getTextToInsert(vimState: VimState): Promise<string> {
|
||||
if ('file' in this.arguments) {
|
||||
return readFileAsync(this.arguments.file, 'utf8');
|
||||
} else if ('cmd' in this.arguments) {
|
||||
if (this.arguments.cmd.length > 0) {
|
||||
if (SUPPORT_READ_COMMAND) {
|
||||
const cmd = this.arguments.cmd;
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
const { exec } = await import('child_process');
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
// TODO: error message?
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
return vimState.document.getText();
|
||||
}
|
||||
}
|
||||
}
|
28
src/cmd_line/commands/redo.ts
Normal file
28
src/cmd_line/commands/redo.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { CommandRedo } from '../../actions/commands/actions';
|
||||
import { Position } from 'vscode';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { optWhitespace, Parser } from 'parsimmon';
|
||||
import { numberParser } from '../../vimscript/parserUtils';
|
||||
|
||||
//
|
||||
// Implements :red[o]
|
||||
// http://vimdoc.sourceforge.net/htmldoc/undo.html#redo
|
||||
//
|
||||
export class RedoCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<RedoCommand> = optWhitespace
|
||||
.then(numberParser)
|
||||
.fallback(undefined)
|
||||
.map((count) => new RedoCommand(count));
|
||||
|
||||
private count?: number;
|
||||
private constructor(count?: number) {
|
||||
super();
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
// TODO: Use `this.count`
|
||||
await new CommandRedo().exec(new Position(0, 0), vimState);
|
||||
}
|
||||
}
|
91
src/cmd_line/commands/register.ts
Normal file
91
src/cmd_line/commands/register.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { Parser, any, optWhitespace } from 'parsimmon';
|
||||
import { ErrorCode, VimError } from '../../error';
|
||||
import { Register } from '../../register/register';
|
||||
import { RecordedState } from '../../state/recordedState';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
|
||||
export class RegisterCommand extends ExCommand {
|
||||
public override isRepeatableWithDot: boolean = false;
|
||||
|
||||
public static readonly argParser: Parser<RegisterCommand> = optWhitespace.then(
|
||||
// eslint-disable-next-line id-denylist
|
||||
any.sepBy(optWhitespace).map((registers) => new RegisterCommand(registers)),
|
||||
);
|
||||
|
||||
private readonly registers: string[];
|
||||
constructor(registers: string[]) {
|
||||
super();
|
||||
this.registers = registers;
|
||||
}
|
||||
|
||||
private async getRegisterDisplayValue(register: string): Promise<string | undefined> {
|
||||
let result = (await Register.get(register))?.text;
|
||||
if (result instanceof Array) {
|
||||
result = result.join('\n').substr(0, 100);
|
||||
} else if (result instanceof RecordedState) {
|
||||
result = result.actionsRun.map((x) => x.keysPressed.join('')).join('');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async displayRegisterValue(vimState: VimState, register: string): Promise<void> {
|
||||
let result = await this.getRegisterDisplayValue(register);
|
||||
if (result === undefined) {
|
||||
StatusBar.displayError(vimState, VimError.fromCode(ErrorCode.NothingInRegister, register));
|
||||
} else {
|
||||
result = result.replace(/\n/g, '\\n');
|
||||
void vscode.window.showInformationMessage(`${register} ${result}`);
|
||||
}
|
||||
}
|
||||
|
||||
private regSortOrder(register: string): number {
|
||||
const specials = ['-', '*', '+', '.', ':', '%', '#', '/', '='];
|
||||
if (register === '"') {
|
||||
return 0;
|
||||
} else if (register >= '0' && register <= '9') {
|
||||
return 10 + parseInt(register, 10);
|
||||
} else if (register >= 'a' && register <= 'z') {
|
||||
return 100 + (register.charCodeAt(0) - 'a'.charCodeAt(0));
|
||||
} else if (specials.includes(register)) {
|
||||
return 1000 + specials.indexOf(register);
|
||||
} else {
|
||||
throw new Error(`Unexpected register ${register}`);
|
||||
}
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
if (this.registers.length === 1) {
|
||||
await this.displayRegisterValue(vimState, this.registers[0]);
|
||||
} else {
|
||||
const currentRegisterKeys = Register.getKeys()
|
||||
.filter(
|
||||
(reg) => reg !== '_' && (this.registers.length === 0 || this.registers.includes(reg)),
|
||||
)
|
||||
.sort((reg1: string, reg2: string) => this.regSortOrder(reg1) - this.regSortOrder(reg2));
|
||||
const registerKeyAndContent = new Array<vscode.QuickPickItem>();
|
||||
|
||||
for (const registerKey of currentRegisterKeys) {
|
||||
const displayValue = await this.getRegisterDisplayValue(registerKey);
|
||||
if (typeof displayValue === 'string') {
|
||||
registerKeyAndContent.push({
|
||||
label: registerKey,
|
||||
description: displayValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void vscode.window.showQuickPick(registerKeyAndContent).then(async (val) => {
|
||||
if (val) {
|
||||
const result = val.description;
|
||||
void vscode.window.showInformationMessage(`${val.label} ${result}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
174
src/cmd_line/commands/retab.ts
Normal file
174
src/cmd_line/commands/retab.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import { optWhitespace, Parser, seq } from 'parsimmon';
|
||||
import { Range } from 'vscode';
|
||||
import { configuration } from '../../configuration/configuration';
|
||||
import { isVisualMode } from '../../mode/mode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { LineRange } from '../../vimscript/lineRange';
|
||||
import { bangParser, numberParser } from '../../vimscript/parserUtils';
|
||||
import { SetCommand } from './set';
|
||||
|
||||
export interface IRetabCommandArguments {
|
||||
replaceSpaces: boolean;
|
||||
newTabstop?: number;
|
||||
}
|
||||
|
||||
interface UpdatedLineSegment {
|
||||
value: string;
|
||||
length: number;
|
||||
}
|
||||
|
||||
// :[range]ret[ab][!] [new_tabstop]
|
||||
export class RetabCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<RetabCommand> = seq(
|
||||
bangParser,
|
||||
optWhitespace.then(numberParser).fallback(undefined),
|
||||
).map(
|
||||
([replaceSpaces, newTabstop]) =>
|
||||
new RetabCommand({
|
||||
replaceSpaces,
|
||||
newTabstop,
|
||||
}),
|
||||
);
|
||||
|
||||
private readonly arguments: IRetabCommandArguments;
|
||||
|
||||
constructor(args: IRetabCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
if (isVisualMode(vimState.currentMode)) {
|
||||
const { start, end } = vimState.editor.selection;
|
||||
this.retab(vimState, start.line, end.line);
|
||||
} else {
|
||||
this.retab(vimState, 0, vimState.document.lineCount - 1);
|
||||
}
|
||||
}
|
||||
|
||||
override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
const { start, end } = range.resolve(vimState);
|
||||
this.retab(vimState, start, end);
|
||||
}
|
||||
|
||||
private concat(count: number, char: string): string {
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
result += char;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private hasTabs(str: string): boolean {
|
||||
return str.indexOf('\t') >= 0;
|
||||
}
|
||||
|
||||
expandtab(str: string, start = 0, tabstop = configuration.tabstop): string {
|
||||
let expanded = '';
|
||||
|
||||
let i = start;
|
||||
for (const char of str) {
|
||||
if (char === '\t') {
|
||||
const spaces = tabstop - (i % tabstop) || tabstop;
|
||||
|
||||
expanded += this.concat(spaces, ' ');
|
||||
i += spaces;
|
||||
} else {
|
||||
expanded += char;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return expanded;
|
||||
}
|
||||
|
||||
retabLineSegment(
|
||||
segment: string,
|
||||
start: number,
|
||||
tabstop = configuration.tabstop,
|
||||
): UpdatedLineSegment {
|
||||
const retab = this.arguments.replaceSpaces || this.hasTabs(segment);
|
||||
|
||||
if (!retab) {
|
||||
return {
|
||||
value: segment,
|
||||
length: segment.length,
|
||||
};
|
||||
}
|
||||
|
||||
const retabTabstop = this.arguments.newTabstop || tabstop;
|
||||
const detabbed = this.expandtab(segment, start, tabstop);
|
||||
|
||||
const spaces = Math.min((start + detabbed.length) % retabTabstop, detabbed.length);
|
||||
const tabs = Math.ceil((detabbed.length - spaces) / retabTabstop);
|
||||
|
||||
let result = '';
|
||||
|
||||
result += this.concat(tabs, '\t');
|
||||
result += this.concat(spaces, ' ');
|
||||
|
||||
return {
|
||||
value: result,
|
||||
length: detabbed.length,
|
||||
};
|
||||
}
|
||||
|
||||
retabLine(line: string, tabstop = configuration.tabstop): string {
|
||||
const segments = line.split(/(\s+)/);
|
||||
let i = 0;
|
||||
|
||||
let retabbed = '';
|
||||
for (const str of segments) {
|
||||
if (!str) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (![' ', '\t'].includes(str[0])) {
|
||||
retabbed += str;
|
||||
i += str.length;
|
||||
} else {
|
||||
const result = this.retabLineSegment(str, i, tabstop);
|
||||
|
||||
retabbed += result.value;
|
||||
i += result.length;
|
||||
}
|
||||
}
|
||||
|
||||
return retabbed;
|
||||
}
|
||||
|
||||
public retab(vimState: VimState, startLine: number, endLine: number) {
|
||||
const originalLines: string[] = [];
|
||||
|
||||
const lastLine = Math.min(endLine, vimState.document.lineCount - 1);
|
||||
for (let i = startLine; i <= lastLine; i++) {
|
||||
originalLines.push(vimState.document.lineAt(i).text);
|
||||
}
|
||||
|
||||
const replacedLines = originalLines.map((line: string) => {
|
||||
return configuration.expandtab ? this.expandtab(line) : this.retabLine(line);
|
||||
});
|
||||
|
||||
const replacedContent = replacedLines.join('\n');
|
||||
const lastLineLength = originalLines[originalLines.length - 1].length;
|
||||
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'replaceText',
|
||||
range: new Range(startLine, 0, endLine, lastLineLength),
|
||||
text: replacedContent,
|
||||
});
|
||||
|
||||
if (this.arguments.newTabstop) {
|
||||
const setTabstop = new SetCommand({
|
||||
type: 'equal',
|
||||
option: 'tabstop',
|
||||
value: this.arguments.newTabstop.toString(),
|
||||
});
|
||||
|
||||
void setTabstop.execute(vimState);
|
||||
}
|
||||
}
|
||||
}
|
300
src/cmd_line/commands/set.ts
Normal file
300
src/cmd_line/commands/set.ts
Normal file
@ -0,0 +1,300 @@
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { alt, oneOf, Parser, regexp, seq, string, whitespace } from 'parsimmon';
|
||||
import { configuration, optionAliases } from '../../configuration/configuration';
|
||||
import { ErrorCode, VimError } from '../../error';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
|
||||
type SetOperation =
|
||||
| {
|
||||
// :se[t]
|
||||
// :se[t] {option}
|
||||
type: 'show_or_set';
|
||||
option: string | undefined;
|
||||
}
|
||||
| {
|
||||
// :se[t] {option}?
|
||||
type: 'show';
|
||||
option: string;
|
||||
}
|
||||
| {
|
||||
// :se[t] no{option}
|
||||
type: 'unset';
|
||||
option: string;
|
||||
}
|
||||
| {
|
||||
// :se[t] {option}!
|
||||
// :se[t] inv{option}
|
||||
type: 'invert';
|
||||
option: string;
|
||||
}
|
||||
| {
|
||||
// :se[t] {option}&
|
||||
// :se[t] {option}&vi
|
||||
// :se[t] {option}&vim
|
||||
type: 'default';
|
||||
option: string;
|
||||
source: 'vi' | 'vim' | '';
|
||||
}
|
||||
| {
|
||||
// :se[t] {option}={value}
|
||||
// :se[t] {option}:{value}
|
||||
type: 'equal';
|
||||
option: string;
|
||||
value: string;
|
||||
}
|
||||
| {
|
||||
// :se[t] {option}+={value}
|
||||
type: 'add';
|
||||
option: string;
|
||||
value: string;
|
||||
}
|
||||
| {
|
||||
// :se[t] {option}^={value}
|
||||
type: 'multiply';
|
||||
option: string;
|
||||
value: string;
|
||||
}
|
||||
| {
|
||||
// :se[t] {option}-={value}
|
||||
type: 'subtract';
|
||||
option: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const optionParser = regexp(/[a-z]+/);
|
||||
const valueParser = regexp(/\S*/);
|
||||
const setOperationParser: Parser<SetOperation> = whitespace
|
||||
.then(
|
||||
alt<SetOperation>(
|
||||
string('no')
|
||||
.then(optionParser)
|
||||
.map((option) => {
|
||||
return {
|
||||
type: 'unset',
|
||||
option,
|
||||
};
|
||||
}),
|
||||
string('inv')
|
||||
.then(optionParser)
|
||||
.map((option) => {
|
||||
return {
|
||||
type: 'invert',
|
||||
option,
|
||||
};
|
||||
}),
|
||||
optionParser.skip(string('!')).map((option) => {
|
||||
return {
|
||||
type: 'invert',
|
||||
option,
|
||||
};
|
||||
}),
|
||||
optionParser.skip(string('?')).map((option) => {
|
||||
return {
|
||||
type: 'show',
|
||||
option,
|
||||
};
|
||||
}),
|
||||
seq(optionParser.skip(string('&')), alt(string('vim'), string('vi'), string(''))).map(
|
||||
([option, source]) => {
|
||||
return {
|
||||
type: 'default',
|
||||
option,
|
||||
source,
|
||||
};
|
||||
},
|
||||
),
|
||||
seq(optionParser.skip(oneOf('=:')), valueParser).map(([option, value]) => {
|
||||
return {
|
||||
type: 'equal',
|
||||
option,
|
||||
value,
|
||||
};
|
||||
}),
|
||||
seq(optionParser.skip(string('+=')), valueParser).map(([option, value]) => {
|
||||
return {
|
||||
type: 'add',
|
||||
option,
|
||||
value,
|
||||
};
|
||||
}),
|
||||
seq(optionParser.skip(string('^=')), valueParser).map(([option, value]) => {
|
||||
return {
|
||||
type: 'multiply',
|
||||
option,
|
||||
value,
|
||||
};
|
||||
}),
|
||||
seq(optionParser.skip(string('-=')), valueParser).map(([option, value]) => {
|
||||
return {
|
||||
type: 'subtract',
|
||||
option,
|
||||
value,
|
||||
};
|
||||
}),
|
||||
optionParser.map((option) => {
|
||||
return {
|
||||
type: 'show_or_set',
|
||||
option,
|
||||
};
|
||||
}),
|
||||
),
|
||||
)
|
||||
.fallback({ type: 'show_or_set', option: undefined });
|
||||
|
||||
export class SetCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<SetCommand> = setOperationParser.map(
|
||||
(operation) => new SetCommand(operation),
|
||||
);
|
||||
|
||||
private readonly operation: SetOperation;
|
||||
constructor(operation: SetOperation) {
|
||||
super();
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
if (this.operation.option === undefined) {
|
||||
// TODO: Show all options that differ from their default value
|
||||
return;
|
||||
}
|
||||
|
||||
const option = optionAliases.get(this.operation.option) ?? this.operation.option;
|
||||
const currentValue = configuration[option] as string | number | boolean | undefined;
|
||||
if (currentValue === undefined) {
|
||||
throw VimError.fromCode(ErrorCode.UnknownOption, option);
|
||||
}
|
||||
const type =
|
||||
typeof currentValue === 'boolean'
|
||||
? 'boolean'
|
||||
: typeof currentValue === 'string'
|
||||
? 'string'
|
||||
: 'number';
|
||||
|
||||
switch (this.operation.type) {
|
||||
case 'show_or_set': {
|
||||
if (this.operation.option === 'all') {
|
||||
// TODO: Show all options
|
||||
} else {
|
||||
if (type === 'boolean') {
|
||||
configuration[option] = true;
|
||||
} else {
|
||||
this.showOption(vimState, option, currentValue);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'show': {
|
||||
this.showOption(vimState, option, currentValue);
|
||||
break;
|
||||
}
|
||||
case 'unset': {
|
||||
if (type === 'boolean') {
|
||||
configuration[option] = false;
|
||||
} else {
|
||||
throw VimError.fromCode(ErrorCode.InvalidArgument, `no${option}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'invert': {
|
||||
if (type === 'boolean') {
|
||||
configuration[option] = !currentValue;
|
||||
} else {
|
||||
// TODO: Could also be {option}!
|
||||
throw VimError.fromCode(ErrorCode.InvalidArgument, `inv${option}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'default': {
|
||||
if (this.operation.option === 'all') {
|
||||
// TODO: Set all options to default
|
||||
} else {
|
||||
// TODO: Set the option to default
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'equal': {
|
||||
if (type === 'boolean') {
|
||||
// TODO: Could also be {option}:{value}
|
||||
throw VimError.fromCode(ErrorCode.InvalidArgument, `${option}=${this.operation.value}`);
|
||||
} else if (type === 'string') {
|
||||
configuration[option] = this.operation.value;
|
||||
} else {
|
||||
const value = Number.parseInt(this.operation.value, 10);
|
||||
if (isNaN(value)) {
|
||||
// TODO: Could also be {option}:{value}
|
||||
throw VimError.fromCode(
|
||||
ErrorCode.NumberRequiredAfterEqual,
|
||||
`${option}=${this.operation.value}`,
|
||||
);
|
||||
}
|
||||
configuration[option] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'add': {
|
||||
if (type === 'boolean') {
|
||||
throw VimError.fromCode(ErrorCode.InvalidArgument, `${option}+=${this.operation.value}`);
|
||||
} else if (type === 'string') {
|
||||
configuration[option] = currentValue + this.operation.value;
|
||||
} else {
|
||||
const value = Number.parseInt(this.operation.value, 10);
|
||||
if (isNaN(value)) {
|
||||
throw VimError.fromCode(
|
||||
ErrorCode.NumberRequiredAfterEqual,
|
||||
`${option}+=${this.operation.value}`,
|
||||
);
|
||||
}
|
||||
configuration[option] = (currentValue as number) + value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'multiply': {
|
||||
if (type === 'boolean') {
|
||||
throw VimError.fromCode(ErrorCode.InvalidArgument, `${option}^=${this.operation.value}`);
|
||||
} else if (type === 'string') {
|
||||
configuration[option] = this.operation.value + currentValue;
|
||||
} else {
|
||||
const value = Number.parseInt(this.operation.value, 10);
|
||||
if (isNaN(value)) {
|
||||
throw VimError.fromCode(
|
||||
ErrorCode.NumberRequiredAfterEqual,
|
||||
`${option}^=${this.operation.value}`,
|
||||
);
|
||||
}
|
||||
configuration[option] = (currentValue as number) * value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'subtract': {
|
||||
if (type === 'boolean') {
|
||||
throw VimError.fromCode(ErrorCode.InvalidArgument, `${option}-=${this.operation.value}`);
|
||||
} else if (type === 'string') {
|
||||
configuration[option] = (currentValue as string).split(this.operation.value).join('');
|
||||
} else {
|
||||
const value = Number.parseInt(this.operation.value, 10);
|
||||
if (isNaN(value)) {
|
||||
throw VimError.fromCode(
|
||||
ErrorCode.NumberRequiredAfterEqual,
|
||||
`${option}-=${this.operation.value}`,
|
||||
);
|
||||
}
|
||||
configuration[option] = (currentValue as number) - value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
const guard: never = this.operation;
|
||||
throw new Error('Got unexpected SetOperation.type');
|
||||
}
|
||||
}
|
||||
|
||||
private showOption(vimState: VimState, option: string, value: boolean | string | number) {
|
||||
if (typeof value === 'boolean') {
|
||||
StatusBar.setText(vimState, value ? option : `no${option}`);
|
||||
} else {
|
||||
StatusBar.setText(vimState, `${option}=${value}`);
|
||||
}
|
||||
}
|
||||
}
|
9
src/cmd_line/commands/sh.ts
Normal file
9
src/cmd_line/commands/sh.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { window } from 'vscode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
|
||||
export class ShCommand extends ExCommand {
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
window.createTerminal().show();
|
||||
}
|
||||
}
|
62
src/cmd_line/commands/shift.ts
Normal file
62
src/cmd_line/commands/shift.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Position, Selection } from 'vscode';
|
||||
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { optWhitespace, Parser, seq, string } from 'parsimmon';
|
||||
import { PositionDiff } from '../../common/motion/position';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { Address, LineRange } from '../../vimscript/lineRange';
|
||||
import { numberParser } from '../../vimscript/parserUtils';
|
||||
|
||||
export type ShiftDirection = '>' | '<';
|
||||
export type ShiftArgs = {
|
||||
dir: '>' | '<';
|
||||
depth: number;
|
||||
numLines: number | undefined;
|
||||
};
|
||||
|
||||
export class ShiftCommand extends ExCommand {
|
||||
public static readonly argParser = (dir: '>' | '<'): Parser<ShiftCommand> =>
|
||||
optWhitespace
|
||||
.then(
|
||||
seq(
|
||||
// `:>>>` indents 3 times `shiftwidth`
|
||||
string(dir)
|
||||
.many()
|
||||
.map((shifts) => shifts.length + 1)
|
||||
.skip(optWhitespace),
|
||||
// `:> 2` indents 2 lines
|
||||
numberParser.fallback(undefined),
|
||||
),
|
||||
)
|
||||
.map(([depth, numLines]) => new ShiftCommand({ dir, depth, numLines }));
|
||||
|
||||
private args: ShiftArgs;
|
||||
constructor(args: ShiftArgs) {
|
||||
super();
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
public async execute(vimState: VimState): Promise<void> {
|
||||
void this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' })));
|
||||
}
|
||||
|
||||
public override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
let { start, end } = range.resolve(vimState);
|
||||
if (this.args.numLines !== undefined) {
|
||||
start = end;
|
||||
end = start + this.args.numLines;
|
||||
}
|
||||
|
||||
vimState.editor.selection = new Selection(new Position(start, 0), new Position(end, 0));
|
||||
for (let i = 0; i < this.args.depth; i++) {
|
||||
if (this.args.dir === '>') {
|
||||
vimState.recordedState.transformer.vscodeCommand('editor.action.indentLines');
|
||||
} else if (this.args.dir === '<') {
|
||||
vimState.recordedState.transformer.vscodeCommand('editor.action.outdentLines');
|
||||
}
|
||||
}
|
||||
|
||||
vimState.recordedState.transformer.moveCursor(PositionDiff.startOfLine());
|
||||
}
|
||||
}
|
43
src/cmd_line/commands/smile.ts
Normal file
43
src/cmd_line/commands/smile.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { TextEditor } from '../../textEditor';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
|
||||
export class SmileCommand extends ExCommand {
|
||||
static readonly smileText: string = `
|
||||
oooo$$$$$$$$$$$$oooo
|
||||
oo$$$$$$$$$$$$$$$$$$$$$$$$o
|
||||
oo$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o o$ $$ o$
|
||||
o $ oo o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o $$ $$ $$o$
|
||||
oo $ $ "$ o$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$o $$$o$$o$
|
||||
"$$$$$$o$ o$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$o $$$$$$$$
|
||||
$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$
|
||||
$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$$$$$$ """$$$
|
||||
"$$$""""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$
|
||||
$$$ o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$o
|
||||
o$$" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$o
|
||||
$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" "$$$$$$ooooo$$$$o
|
||||
o$$$oooo$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o$$$$$$$$$$$$$$$$$
|
||||
$$$$$$$$"$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$""""""""
|
||||
"""" $$$$ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" o$$$
|
||||
"$$$o """$$$$$$$$$$$$$$$$$$"$$" $$$
|
||||
$$$o "$$""$$$$$$"""" o$$$
|
||||
$$$$o o$$$"
|
||||
"$$$$o o$$$$$$o"$$$$o o$$$$
|
||||
"$$$$$oo ""$$$$o$$$$$o o$$$$""
|
||||
""$$$$$oooo "$$$o$$$$$$$$$"""
|
||||
""$$$$$$$oo $$$$$$$$$$
|
||||
""""$$$$$$$$$$$
|
||||
$$$$$$$$$$$$
|
||||
$$$$$$$$$$"
|
||||
"$$$""""`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
await vscode.commands.executeCommand('workbench.action.files.newUntitledFile');
|
||||
await TextEditor.insert(vscode.window.activeTextEditor!, SmileCommand.smileText);
|
||||
}
|
||||
}
|
114
src/cmd_line/commands/sort.ts
Normal file
114
src/cmd_line/commands/sort.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { oneOf, optWhitespace, Parser, seq } from 'parsimmon';
|
||||
import { NumericString, NumericStringRadix } from '../../common/number/numericString';
|
||||
import * as vscode from 'vscode';
|
||||
import { PositionDiff } from '../../common/motion/position';
|
||||
|
||||
import { isVisualMode } from '../../mode/mode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { LineRange } from '../../vimscript/lineRange';
|
||||
import { bangParser } from '../../vimscript/parserUtils';
|
||||
|
||||
export interface ISortCommandArguments {
|
||||
reverse: boolean;
|
||||
ignoreCase: boolean;
|
||||
unique: boolean;
|
||||
numeric: boolean;
|
||||
// TODO: support other flags
|
||||
// TODO(#6676): support pattern
|
||||
}
|
||||
|
||||
export class SortCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<SortCommand> = seq(
|
||||
bangParser,
|
||||
optWhitespace.then(oneOf('bfilnorux').many()),
|
||||
).map(
|
||||
([bang, flags]) =>
|
||||
new SortCommand({
|
||||
reverse: bang,
|
||||
ignoreCase: flags.includes('i'),
|
||||
unique: flags.includes('u'),
|
||||
numeric: flags.includes('n'),
|
||||
}),
|
||||
);
|
||||
|
||||
private readonly arguments: ISortCommandArguments;
|
||||
constructor(args: ISortCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
public override neovimCapable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
if (isVisualMode(vimState.currentMode)) {
|
||||
const { start, end } = vimState.editor.selection;
|
||||
await this.sortLines(vimState, start.line, end.line);
|
||||
} else {
|
||||
await this.sortLines(vimState, 0, vimState.document.lineCount - 1);
|
||||
}
|
||||
}
|
||||
|
||||
async sortLines(vimState: VimState, startLine: number, endLine: number) {
|
||||
let originalLines: string[] = [];
|
||||
|
||||
for (
|
||||
let currentLine = startLine;
|
||||
currentLine <= endLine && currentLine < vimState.document.lineCount;
|
||||
currentLine++
|
||||
) {
|
||||
originalLines.push(vimState.document.lineAt(currentLine).text);
|
||||
}
|
||||
|
||||
const lastLineLength = originalLines[originalLines.length - 1].length;
|
||||
|
||||
if (this.arguments.unique) {
|
||||
const seen = new Set<string>();
|
||||
const uniqueLines: string[] = [];
|
||||
for (const line of originalLines) {
|
||||
const adjustedLine = this.arguments.ignoreCase ? line.toLowerCase() : line;
|
||||
if (!seen.has(adjustedLine)) {
|
||||
seen.add(adjustedLine);
|
||||
uniqueLines.push(line);
|
||||
}
|
||||
}
|
||||
originalLines = uniqueLines;
|
||||
}
|
||||
|
||||
let sortedLines;
|
||||
if (this.arguments.numeric) {
|
||||
sortedLines = originalLines.sort(
|
||||
(a: string, b: string) =>
|
||||
(NumericString.parse(a, NumericStringRadix.Dec)?.num.value ?? Number.MAX_VALUE) -
|
||||
(NumericString.parse(b, NumericStringRadix.Dec)?.num.value ?? Number.MAX_VALUE),
|
||||
);
|
||||
} else if (this.arguments.ignoreCase) {
|
||||
sortedLines = originalLines.sort((a: string, b: string) => a.localeCompare(b));
|
||||
} else {
|
||||
sortedLines = originalLines.sort();
|
||||
}
|
||||
|
||||
if (this.arguments.reverse) {
|
||||
sortedLines.reverse();
|
||||
}
|
||||
|
||||
const sortedContent = sortedLines.join('\n');
|
||||
|
||||
vimState.recordedState.transformer.addTransformation({
|
||||
type: 'replaceText',
|
||||
range: new vscode.Range(startLine, 0, endLine, lastLineLength),
|
||||
text: sortedContent,
|
||||
diff: PositionDiff.exactPosition(
|
||||
new vscode.Position(startLine, sortedLines[0].match(/\S/)?.index ?? 0),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
override async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
|
||||
const { start, end } = range.resolve(vimState);
|
||||
|
||||
await this.sortLines(vimState, start, end);
|
||||
}
|
||||
}
|
638
src/cmd_line/commands/substitute.ts
Normal file
638
src/cmd_line/commands/substitute.ts
Normal file
@ -0,0 +1,638 @@
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { Parser, alt, any, noneOf, oneOf, optWhitespace, regexp, seq, string } from 'parsimmon';
|
||||
import { CancellationTokenSource, DecorationOptions, Position, Range, window } from 'vscode';
|
||||
import { PositionDiff } from '../../common/motion/position';
|
||||
import { configuration } from '../../configuration/configuration';
|
||||
import { decoration } from '../../configuration/decoration';
|
||||
import { ErrorCode, VimError } from '../../error';
|
||||
import { Jump } from '../../jumps/jump';
|
||||
import { globalState } from '../../state/globalState';
|
||||
import { SearchState } from '../../state/searchState';
|
||||
import { SubstituteState } from '../../state/substituteState';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { SearchDecorations, ensureVisible, formatDecorationText } from '../../util/decorationUtils';
|
||||
import { escapeCSSIcons } from '../../util/statusBarTextUtils';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { Address, LineRange } from '../../vimscript/lineRange';
|
||||
import { numberParser } from '../../vimscript/parserUtils';
|
||||
import { Pattern, PatternMatch, SearchDirection } from '../../vimscript/pattern';
|
||||
|
||||
type ReplaceStringComponent =
|
||||
| { type: 'string'; value: string }
|
||||
| { type: 'capture_group'; group: number | '&' }
|
||||
| { type: 'prev_replace_string' }
|
||||
| { type: 'change_case'; case: 'upper' | 'lower'; duration: 'char' | 'until_end' }
|
||||
| { type: 'change_case_end' };
|
||||
|
||||
export class ReplaceString {
|
||||
private components: ReplaceStringComponent[];
|
||||
constructor(components: ReplaceStringComponent[]) {
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.components
|
||||
.map((component) => {
|
||||
if (component.type === 'string') {
|
||||
return component.value;
|
||||
} else if (component.type === 'capture_group') {
|
||||
return component.group === '&' ? '&' : `\\${component.group}`;
|
||||
} else if (component.type === 'prev_replace_string') {
|
||||
return '~';
|
||||
} else if (component.type === 'change_case') {
|
||||
if (component.case === 'upper') {
|
||||
return component.duration === 'char' ? '\\u' : '\\U';
|
||||
} else {
|
||||
return component.duration === 'char' ? '\\l' : '\\L';
|
||||
}
|
||||
} else if (component.type === 'change_case_end') {
|
||||
return '\\E';
|
||||
} else {
|
||||
const guard: never = component;
|
||||
return '';
|
||||
}
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
public resolve(matches: string[]): string {
|
||||
let result = '';
|
||||
let changeCase: 'upper' | 'lower' | undefined;
|
||||
let changeCaseChar: 'upper' | 'lower' | undefined;
|
||||
for (const component of this.components) {
|
||||
let newChangeCaseChar: 'upper' | 'lower' | undefined;
|
||||
let _result: string = '';
|
||||
if (component.type === 'string') {
|
||||
_result = component.value;
|
||||
} else if (component.type === 'capture_group') {
|
||||
const group: number = component.group === '&' ? 0 : component.group;
|
||||
_result = matches[group] ?? '';
|
||||
} else if (component.type === 'prev_replace_string') {
|
||||
_result = globalState.substituteState?.replaceString.toString() ?? '';
|
||||
} else if (component.type === 'change_case') {
|
||||
if (component.duration === 'until_end') {
|
||||
changeCase = component.case;
|
||||
} else {
|
||||
newChangeCaseChar = component.case;
|
||||
}
|
||||
} else if (component.type === 'change_case_end') {
|
||||
changeCase = undefined;
|
||||
} else {
|
||||
const guard: never = component;
|
||||
}
|
||||
|
||||
if (_result) {
|
||||
if (changeCase) {
|
||||
_result =
|
||||
changeCase === 'upper' ? _result.toLocaleUpperCase() : _result.toLocaleLowerCase();
|
||||
}
|
||||
if (changeCaseChar) {
|
||||
_result =
|
||||
(changeCaseChar === 'upper'
|
||||
? _result[0].toLocaleUpperCase()
|
||||
: _result[0].toLocaleLowerCase()) + _result.slice(1);
|
||||
}
|
||||
result += _result;
|
||||
}
|
||||
|
||||
changeCaseChar = newChangeCaseChar;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: for "pattern", undefined is different from an empty string.
|
||||
* when it's undefined, it means to repeat the previous REPLACEMENT and ignore "replace".
|
||||
* when it's an empty string, it means to use the previous SEARCH (not replacement) state,
|
||||
* and replace with whatever's set by "replace" (even an empty string).
|
||||
*/
|
||||
export interface ISubstituteCommandArguments {
|
||||
pattern: Pattern | undefined;
|
||||
replace: ReplaceString;
|
||||
flags: SubstituteFlags;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The flags that you can use for the substitute commands:
|
||||
* [&] Must be the first one: Keep the flags from the previous substitute command.
|
||||
* [c] Confirm each substitution.
|
||||
* [e] When the search pattern fails, do not issue an error message and, in
|
||||
* particular, continue in maps as if no error occurred.
|
||||
* [g] Replace all occurrences in the line. Without this argument, replacement
|
||||
* occurs only for the first occurrence in each line.
|
||||
* [i] Ignore case for the pattern.
|
||||
* [I] Don't ignore case for the pattern.
|
||||
* [n] Report the number of matches, do not actually substitute.
|
||||
* [p] Print the line containing the last substitute.
|
||||
* [#] Like [p] and prepend the line number.
|
||||
* [l] Like [p] but print the text like |:list|.
|
||||
* [r] When the search pattern is empty, use the previously used search pattern
|
||||
* instead of the search pattern from the last substitute or ":global".
|
||||
*/
|
||||
export interface SubstituteFlags {
|
||||
keepPreviousFlags?: true; // TODO: use this flag
|
||||
confirmEach?: true;
|
||||
suppressError?: true; // TODO: use this flag
|
||||
replaceAll?: true;
|
||||
ignoreCase?: true; // TODO: use this flag
|
||||
noIgnoreCase?: true; // TODO: use this flag
|
||||
printCount?: true;
|
||||
// TODO: use the following flags:
|
||||
printLastMatchedLine?: true;
|
||||
printLastMatchedLineWithNumber?: true;
|
||||
printLastMatchedLineWithList?: true;
|
||||
usePreviousPattern?: true;
|
||||
}
|
||||
|
||||
// TODO: `:help sub-replace-special`
|
||||
// TODO: `:help sub-replace-expression`
|
||||
const replaceStringParser = (delimiter: string): Parser<ReplaceString> =>
|
||||
alt<ReplaceStringComponent>(
|
||||
string('\\').then(
|
||||
// eslint-disable-next-line id-denylist
|
||||
any.fallback(undefined).map<ReplaceStringComponent>((escaped) => {
|
||||
if (escaped === undefined || escaped === '\\') {
|
||||
return { type: 'string', value: '\\' };
|
||||
} else if (escaped === '/') {
|
||||
return { type: 'string', value: '/' };
|
||||
} else if (escaped === 'b') {
|
||||
return { type: 'string', value: '\b' };
|
||||
} else if (escaped === 'r') {
|
||||
return { type: 'string', value: '\r' };
|
||||
} else if (escaped === 'n') {
|
||||
return { type: 'string', value: '\n' };
|
||||
} else if (escaped === 't') {
|
||||
return { type: 'string', value: '\t' };
|
||||
} else if (escaped === '&') {
|
||||
return { type: 'string', value: '&' };
|
||||
} else if (escaped === '~') {
|
||||
return { type: 'string', value: '~' };
|
||||
} else if (/[0-9]/.test(escaped)) {
|
||||
return { type: 'capture_group', group: Number.parseInt(escaped, 10) };
|
||||
} else if (escaped === 'u') {
|
||||
return { type: 'change_case', case: 'upper', duration: 'char' };
|
||||
} else if (escaped === 'l') {
|
||||
return { type: 'change_case', case: 'lower', duration: 'char' };
|
||||
} else if (escaped === 'U') {
|
||||
return { type: 'change_case', case: 'upper', duration: 'until_end' };
|
||||
} else if (escaped === 'L') {
|
||||
return { type: 'change_case', case: 'lower', duration: 'until_end' };
|
||||
} else if (escaped === 'e' || escaped === 'E') {
|
||||
return { type: 'change_case_end' };
|
||||
} else {
|
||||
return { type: 'string', value: `\\${escaped}` };
|
||||
}
|
||||
}),
|
||||
),
|
||||
string('&').result({ type: 'capture_group', group: '&' }),
|
||||
string('~').result({ type: 'prev_replace_string' }),
|
||||
noneOf(delimiter).map((value) => ({ type: 'string', value })),
|
||||
)
|
||||
.many()
|
||||
.map((components) => new ReplaceString(components));
|
||||
|
||||
const substituteFlagsParser: Parser<SubstituteFlags> = seq(
|
||||
string('&').fallback(undefined),
|
||||
oneOf('cegiInp#lr').many(),
|
||||
).map(([amp, flagChars]) => {
|
||||
const flags: SubstituteFlags = {};
|
||||
if (amp === '&') {
|
||||
flags.keepPreviousFlags = true;
|
||||
}
|
||||
for (const flag of flagChars) {
|
||||
switch (flag) {
|
||||
case 'c':
|
||||
flags.confirmEach = true;
|
||||
break;
|
||||
case 'e':
|
||||
flags.suppressError = true;
|
||||
break;
|
||||
case 'g':
|
||||
flags.replaceAll = true;
|
||||
break;
|
||||
case 'i':
|
||||
flags.ignoreCase = true;
|
||||
break;
|
||||
case 'I':
|
||||
flags.noIgnoreCase = true;
|
||||
break;
|
||||
case 'n':
|
||||
flags.printCount = true;
|
||||
break;
|
||||
case 'p':
|
||||
flags.printLastMatchedLine = true;
|
||||
break;
|
||||
case '#':
|
||||
flags.printLastMatchedLineWithNumber = true;
|
||||
break;
|
||||
case 'l':
|
||||
flags.printLastMatchedLineWithList = true;
|
||||
break;
|
||||
case 'r':
|
||||
flags.usePreviousPattern = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
});
|
||||
|
||||
const countParser: Parser<number | undefined> = optWhitespace
|
||||
.then(numberParser)
|
||||
.fallback(undefined);
|
||||
|
||||
/**
|
||||
* vim has a distinctly different state for previous search and for previous substitute. However, in SOME
|
||||
* cases a substitution will also update the search state along with the substitute state.
|
||||
*
|
||||
* Also, the substitute command itself will sometimes use the search state, and other times it will use the
|
||||
* substitute state.
|
||||
*
|
||||
* These are the following cases and how vim handles them:
|
||||
* 1. :s/this/that
|
||||
* - standard search/replace
|
||||
* - update substitution state
|
||||
* - update search state too!
|
||||
* 2. :s or :s [flags]
|
||||
* - use previous SUBSTITUTION state, and repeat previous substitution pattern and replace.
|
||||
* - do not touch search state!
|
||||
* - changing substitution state is dont-care cause we're repeating it ;)
|
||||
* 3. :s/ or :s// or :s///
|
||||
* - use previous SEARCH state (not substitution), and DELETE the string matching the pattern (replace with nothing)
|
||||
* - update substitution state
|
||||
* - updating search state is dont-care cause we're repeating it ;)
|
||||
* 4. :s/this or :s/this/ or :s/this//
|
||||
* - input is pattern - replacement is empty (delete)
|
||||
* - update replacement state
|
||||
* - update search state too!
|
||||
*/
|
||||
export class SubstituteCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<SubstituteCommand> = optWhitespace.then(
|
||||
alt(
|
||||
// :s[ubstitute]/{pattern}/{string}/[flags] [count]
|
||||
regexp(/[^\w\s\\|"]{1}/).chain((delimiter) =>
|
||||
seq(
|
||||
Pattern.parser({ direction: SearchDirection.Forward, delimiter }),
|
||||
replaceStringParser(delimiter),
|
||||
string(delimiter).then(substituteFlagsParser).fallback({}),
|
||||
countParser,
|
||||
).map(
|
||||
([pattern, replace, flags, count]) =>
|
||||
new SubstituteCommand({ pattern, replace, flags, count }),
|
||||
),
|
||||
),
|
||||
|
||||
// :s[ubstitute] [flags] [count]
|
||||
seq(substituteFlagsParser, countParser).map(
|
||||
([flags, count]) =>
|
||||
new SubstituteCommand({
|
||||
pattern: undefined,
|
||||
replace: new ReplaceString([]),
|
||||
flags,
|
||||
count,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
public readonly arguments: ISubstituteCommandArguments;
|
||||
protected abort: boolean;
|
||||
private cSearchHighlights?: DecorationOptions[];
|
||||
private confirmedSubstitutions?: DecorationOptions[];
|
||||
constructor(args: ISubstituteCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
this.abort = false;
|
||||
}
|
||||
|
||||
public override neovimCapable(): boolean {
|
||||
// We need to use VSCode's quickpick capabilities to do confirmation
|
||||
return !this.arguments.flags.confirmEach;
|
||||
}
|
||||
|
||||
public getSubstitutionDecorations(
|
||||
vimState: VimState,
|
||||
lineRange = new LineRange(new Address({ type: 'current_line' })),
|
||||
): SearchDecorations {
|
||||
const substitutionAppend: DecorationOptions[] = [];
|
||||
const substitutionReplace: DecorationOptions[] = [];
|
||||
const searchHighlight: DecorationOptions[] = [];
|
||||
|
||||
const subsArr: DecorationOptions[] =
|
||||
configuration.inccommand === 'replace' ? substitutionReplace : substitutionAppend;
|
||||
|
||||
const { pattern, replace } = this.resolvePatterns(false);
|
||||
|
||||
const showReplacements =
|
||||
this.arguments.pattern?.closed &&
|
||||
configuration.inccommand &&
|
||||
!this.arguments.flags.printCount;
|
||||
|
||||
let matches: PatternMatch[] = [];
|
||||
if (pattern?.patternString) {
|
||||
matches = pattern.allMatches(vimState, { lineRange });
|
||||
}
|
||||
|
||||
const global =
|
||||
(configuration.gdefault || configuration.substituteGlobalFlag) !==
|
||||
(this.arguments.flags.replaceAll ?? false);
|
||||
const lines = new Set<number>();
|
||||
|
||||
for (const match of matches) {
|
||||
if (!global && lines.has(match.range.start.line)) {
|
||||
// If not global, only replace one match per line
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.add(match.range.start.line);
|
||||
if (showReplacements) {
|
||||
const contentText = formatDecorationText(
|
||||
replace.resolve(match.groups),
|
||||
vimState.editor.options.tabSize as number,
|
||||
);
|
||||
|
||||
subsArr.push({
|
||||
range: match.range,
|
||||
renderOptions: {
|
||||
[configuration.inccommand === 'append' ? 'after' : 'before']: { contentText },
|
||||
},
|
||||
});
|
||||
} else {
|
||||
searchHighlight.push(ensureVisible(match.range));
|
||||
}
|
||||
}
|
||||
return { substitutionAppend, substitutionReplace, searchHighlight };
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns If match, (# newlines added) - (# newlines removed)
|
||||
* Else, undefined
|
||||
*/
|
||||
private async replaceMatchRange(
|
||||
vimState: VimState,
|
||||
match: PatternMatch,
|
||||
): Promise<number | undefined> {
|
||||
if (this.arguments.flags.printCount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const replaceText = this.arguments.replace.resolve(match.groups);
|
||||
|
||||
if (this.arguments.flags.confirmEach) {
|
||||
if (await this.confirmReplacement(vimState, match, replaceText)) {
|
||||
vimState.recordedState.transformer.replace(match.range, replaceText);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
vimState.recordedState.transformer.replace(match.range, replaceText);
|
||||
}
|
||||
|
||||
const addedNewlines = replaceText.split('\n').length - 1;
|
||||
const removedNewlines = match.groups[0].split('\n').length - 1;
|
||||
return addedNewlines - removedNewlines;
|
||||
}
|
||||
|
||||
private async confirmReplacement(
|
||||
vimState: VimState,
|
||||
match: PatternMatch,
|
||||
replaceText: string,
|
||||
): Promise<boolean> {
|
||||
const cancellationToken = new CancellationTokenSource();
|
||||
const validSelections: readonly string[] = ['y', 'n', 'a', 'q', 'l'];
|
||||
let selection: string = '';
|
||||
const prompt = escapeCSSIcons(
|
||||
`Replace with ${formatDecorationText(
|
||||
replaceText,
|
||||
vimState.editor.options.tabSize as number,
|
||||
'\\n',
|
||||
)} (${validSelections.join('/')})?`,
|
||||
);
|
||||
|
||||
const newConfirmationSearchHighlights =
|
||||
this.cSearchHighlights?.filter((d) => !d.range.isEqual(match.range)) ?? [];
|
||||
|
||||
vimState.editor.revealRange(new Range(match.range.start.line, 0, match.range.start.line, 0));
|
||||
vimState.editor.setDecorations(decoration.searchHighlight, newConfirmationSearchHighlights);
|
||||
vimState.editor.setDecorations(decoration.searchMatch, [ensureVisible(match.range)]);
|
||||
vimState.editor.setDecorations(
|
||||
decoration.confirmedSubstitution,
|
||||
this.confirmedSubstitutions ?? [],
|
||||
);
|
||||
await window.showInputBox(
|
||||
{
|
||||
ignoreFocusOut: true,
|
||||
prompt,
|
||||
placeHolder: validSelections.join('/'),
|
||||
validateInput: (input: string): string => {
|
||||
if (validSelections.includes(input)) {
|
||||
selection = input;
|
||||
cancellationToken.cancel();
|
||||
}
|
||||
return prompt;
|
||||
},
|
||||
},
|
||||
cancellationToken.token,
|
||||
);
|
||||
|
||||
if (selection === 'q' || selection === 'l' || !selection) {
|
||||
this.abort = true;
|
||||
} else if (selection === 'a') {
|
||||
this.arguments.flags.confirmEach = undefined;
|
||||
}
|
||||
|
||||
if (selection === 'y' || selection === 'a' || selection === 'l') {
|
||||
if (this.cSearchHighlights) {
|
||||
this.cSearchHighlights = newConfirmationSearchHighlights;
|
||||
}
|
||||
|
||||
this.confirmedSubstitutions?.push({
|
||||
range: match.range,
|
||||
renderOptions: {
|
||||
before: {
|
||||
contentText: formatDecorationText(
|
||||
replaceText,
|
||||
vimState.editor.options.tabSize as number,
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the concrete Pattern and ReplaceString to be used for this substitution.
|
||||
* If throwErrors is true, errors will be thrown :)
|
||||
*/
|
||||
private resolvePatterns(throwErrors: boolean): {
|
||||
pattern: Pattern | undefined;
|
||||
replace: ReplaceString;
|
||||
} {
|
||||
let { pattern, replace } = this.arguments;
|
||||
if (pattern === undefined) {
|
||||
// If no pattern is entered, use previous SUBSTITUTION state and don't update search state
|
||||
// i.e. :s
|
||||
const prevSubstituteState = globalState.substituteState;
|
||||
if (
|
||||
prevSubstituteState?.searchPattern === undefined ||
|
||||
prevSubstituteState.searchPattern.patternString === ''
|
||||
) {
|
||||
if (throwErrors) {
|
||||
throw VimError.fromCode(ErrorCode.NoPreviousSubstituteRegularExpression);
|
||||
}
|
||||
} else {
|
||||
pattern = prevSubstituteState.searchPattern;
|
||||
replace = prevSubstituteState.replaceString;
|
||||
}
|
||||
} else {
|
||||
if (pattern.patternString === '') {
|
||||
// If an explicitly empty pattern is entered, use previous search state (including search with * and #) and update both states
|
||||
// e.g :s/ or :s///
|
||||
const prevSearchState = globalState.searchState;
|
||||
if (prevSearchState === undefined || prevSearchState.searchString === '') {
|
||||
if (throwErrors) {
|
||||
throw VimError.fromCode(ErrorCode.NoPreviousRegularExpression);
|
||||
}
|
||||
} else {
|
||||
pattern = prevSearchState.pattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { pattern, replace };
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
await this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' })));
|
||||
}
|
||||
|
||||
override async executeWithRange(vimState: VimState, lineRange: LineRange): Promise<void> {
|
||||
let { start, end } = lineRange.resolve(vimState);
|
||||
|
||||
if (this.arguments.count && this.arguments.count >= 0) {
|
||||
start = end;
|
||||
end = end + this.arguments.count - 1;
|
||||
}
|
||||
|
||||
// TODO: this is all a bit gross
|
||||
const { pattern, replace } = this.resolvePatterns(true);
|
||||
this.arguments.replace = replace;
|
||||
|
||||
// `/g` flag inverts the default behavior (from `gdefault`)
|
||||
const global =
|
||||
(configuration.gdefault || configuration.substituteGlobalFlag) !==
|
||||
(this.arguments.flags.replaceAll ?? false);
|
||||
|
||||
// TODO: `allMatches` lies for patterns with empty branches, which makes this wrong (not that anyone cares)
|
||||
const allMatches =
|
||||
pattern?.allMatches(vimState, {
|
||||
// TODO: This method should probably take start/end lines as numbers
|
||||
lineRange: new LineRange(
|
||||
new Address({ type: 'number', num: start + 1 }),
|
||||
',',
|
||||
new Address({ type: 'number', num: end + 1 }),
|
||||
),
|
||||
}) ?? [];
|
||||
|
||||
let replaceableMatches;
|
||||
if (global) {
|
||||
// every match is replaceable
|
||||
replaceableMatches = allMatches;
|
||||
} else {
|
||||
// only the first match on a line is replaceable
|
||||
const replaceableLines = new Set<number>();
|
||||
replaceableMatches = allMatches.filter((match) => {
|
||||
if (replaceableLines.has(match.range.start.line)) {
|
||||
return false;
|
||||
}
|
||||
replaceableLines.add(match.range.start.line);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.arguments.flags.confirmEach) {
|
||||
vimState.editor.setDecorations(decoration.substitutionAppend, []);
|
||||
vimState.editor.setDecorations(decoration.substitutionReplace, []);
|
||||
|
||||
if (configuration.inccommand) {
|
||||
this.confirmedSubstitutions = [];
|
||||
}
|
||||
if (configuration.incsearch) {
|
||||
this.cSearchHighlights = replaceableMatches.map((match) => ensureVisible(match.range));
|
||||
}
|
||||
}
|
||||
|
||||
const substitutionLines = new Set<number>();
|
||||
let substitutions = 0;
|
||||
let netNewLines = 0;
|
||||
|
||||
for (const match of replaceableMatches) {
|
||||
if (this.abort) {
|
||||
break;
|
||||
}
|
||||
|
||||
const newLines = await this.replaceMatchRange(vimState, match);
|
||||
if (newLines !== undefined) {
|
||||
substitutions++;
|
||||
substitutionLines.add(match.range.start.line);
|
||||
netNewLines += newLines;
|
||||
}
|
||||
}
|
||||
|
||||
if (substitutions > 0 && !this.arguments.flags.printCount) {
|
||||
// if any substitutions were made, jump to latest one
|
||||
const lastLine = Math.max(...substitutionLines.values()) + netNewLines;
|
||||
const cursor = new Position(Math.max(0, lastLine), 0);
|
||||
globalState.jumpTracker.recordJump(
|
||||
new Jump({
|
||||
document: vimState.document,
|
||||
position: cursor,
|
||||
}),
|
||||
Jump.fromStateNow(vimState),
|
||||
);
|
||||
vimState.recordedState.transformer.moveCursor(PositionDiff.exactPosition(cursor), 0);
|
||||
}
|
||||
|
||||
this.confirmedSubstitutions = undefined;
|
||||
this.cSearchHighlights = undefined;
|
||||
vimState.editor.setDecorations(decoration.confirmedSubstitution, []);
|
||||
|
||||
this.setStatusBarText(vimState, substitutions, substitutionLines.size);
|
||||
|
||||
if (this.arguments.pattern !== undefined) {
|
||||
globalState.substituteState = new SubstituteState(pattern, replace);
|
||||
globalState.searchState = new SearchState(
|
||||
SearchDirection.Forward,
|
||||
vimState.cursorStopPosition,
|
||||
pattern?.patternString,
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private setStatusBarText(vimState: VimState, substitutions: number, lines: number) {
|
||||
if (substitutions === 0) {
|
||||
StatusBar.displayError(
|
||||
vimState,
|
||||
VimError.fromCode(ErrorCode.PatternNotFound, this.arguments.pattern?.patternString),
|
||||
);
|
||||
} else if (this.arguments.flags.printCount) {
|
||||
StatusBar.setText(
|
||||
vimState,
|
||||
`${substitutions} match${substitutions > 1 ? 'es' : ''} on ${lines} line${
|
||||
lines > 1 ? 's' : ''
|
||||
}`,
|
||||
);
|
||||
} else if (substitutions > configuration.report) {
|
||||
StatusBar.setText(
|
||||
vimState,
|
||||
`${substitutions} substitution${substitutions > 1 ? 's' : ''} on ${lines} line${
|
||||
lines > 1 ? 's' : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
251
src/cmd_line/commands/tab.ts
Normal file
251
src/cmd_line/commands/tab.ts
Normal file
@ -0,0 +1,251 @@
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { alt, optWhitespace, regexp, seq, string, whitespace } from 'parsimmon';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import {
|
||||
FileCmd,
|
||||
FileOpt,
|
||||
bangParser,
|
||||
fileCmdParser,
|
||||
fileOptParser,
|
||||
numberParser,
|
||||
} from '../../vimscript/parserUtils';
|
||||
|
||||
export enum TabCommandType {
|
||||
Next,
|
||||
Previous,
|
||||
First,
|
||||
Last,
|
||||
Absolute,
|
||||
New,
|
||||
Close,
|
||||
Only,
|
||||
Move,
|
||||
}
|
||||
|
||||
// TODO: many of these arguments aren't used
|
||||
export type ITabCommandArguments =
|
||||
| {
|
||||
type: TabCommandType.Absolute;
|
||||
count: number;
|
||||
}
|
||||
| {
|
||||
type: TabCommandType.First | TabCommandType.Last;
|
||||
cmd?: FileCmd;
|
||||
}
|
||||
| {
|
||||
type: TabCommandType.Next | TabCommandType.Previous;
|
||||
bang: boolean;
|
||||
cmd?: FileCmd;
|
||||
count?: number;
|
||||
}
|
||||
| {
|
||||
type: TabCommandType.Close | TabCommandType.Only;
|
||||
bang: boolean;
|
||||
count?: number;
|
||||
}
|
||||
| {
|
||||
type: TabCommandType.New;
|
||||
opt: FileOpt;
|
||||
cmd?: FileCmd;
|
||||
file?: string;
|
||||
}
|
||||
| {
|
||||
type: TabCommandType.Move;
|
||||
direction?: 'left' | 'right';
|
||||
count?: number;
|
||||
};
|
||||
|
||||
//
|
||||
// Implements most buffer and tab ex commands
|
||||
// http://vimdoc.sourceforge.net/htmldoc/tabpage.html
|
||||
//
|
||||
export class TabCommand extends ExCommand {
|
||||
// TODO: `count` is parsed as a number, which is incomplete
|
||||
public static readonly argParsers = {
|
||||
bfirst: whitespace
|
||||
.then(fileCmdParser)
|
||||
.fallback(undefined)
|
||||
.map((cmd) => {
|
||||
return new TabCommand({ type: TabCommandType.First, cmd });
|
||||
}),
|
||||
blast: whitespace
|
||||
.then(fileCmdParser)
|
||||
.fallback(undefined)
|
||||
.map((cmd) => {
|
||||
return new TabCommand({ type: TabCommandType.Last, cmd });
|
||||
}),
|
||||
bnext: seq(
|
||||
bangParser,
|
||||
optWhitespace.then(fileCmdParser).fallback(undefined),
|
||||
optWhitespace.then(numberParser).fallback(undefined),
|
||||
).map(([bang, cmd, count]) => {
|
||||
return new TabCommand({ type: TabCommandType.Next, bang, cmd, count });
|
||||
}),
|
||||
bprev: seq(
|
||||
bangParser,
|
||||
optWhitespace.then(fileCmdParser).fallback(undefined),
|
||||
optWhitespace.then(numberParser).fallback(undefined),
|
||||
).map(([bang, cmd, count]) => {
|
||||
return new TabCommand({ type: TabCommandType.Previous, bang, cmd, count });
|
||||
}),
|
||||
tabclose: seq(bangParser, optWhitespace.then(numberParser).fallback(undefined)).map(
|
||||
([bang, count]) => {
|
||||
return new TabCommand({ type: TabCommandType.Close, bang, count });
|
||||
},
|
||||
),
|
||||
tabonly: seq(bangParser, optWhitespace.then(numberParser).fallback(undefined)).map(
|
||||
([bang, count]) => {
|
||||
return new TabCommand({ type: TabCommandType.Only, bang, count });
|
||||
},
|
||||
),
|
||||
tabnew: seq(
|
||||
optWhitespace.then(fileOptParser).fallback([]),
|
||||
optWhitespace.then(fileCmdParser).fallback(undefined),
|
||||
regexp(/\S+/).fallback(undefined),
|
||||
).map(([opt, cmd, file]) => {
|
||||
return new TabCommand({
|
||||
type: TabCommandType.New,
|
||||
opt,
|
||||
cmd,
|
||||
file,
|
||||
});
|
||||
}),
|
||||
tabmove: optWhitespace
|
||||
.then(
|
||||
seq(
|
||||
alt<'right' | 'left'>(string('+').result('right'), string('-').result('left')).fallback(
|
||||
undefined,
|
||||
),
|
||||
numberParser.fallback(undefined),
|
||||
),
|
||||
)
|
||||
.map(([direction, count]) => new TabCommand({ type: TabCommandType.Move, direction, count })),
|
||||
tabAbsolute: optWhitespace
|
||||
.then(numberParser.fallback(undefined))
|
||||
.map((count) => new TabCommand({ type: TabCommandType.Absolute, count: count ?? 0 })),
|
||||
};
|
||||
|
||||
public readonly arguments: ITabCommandArguments;
|
||||
constructor(args: ITabCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
private async executeCommandWithCount(count: number, command: string): Promise<void> {
|
||||
for (let i = 0; i < count; i++) {
|
||||
await vscode.commands.executeCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
switch (this.arguments.type) {
|
||||
case TabCommandType.Absolute:
|
||||
if (this.arguments.count !== undefined && this.arguments.count >= 0) {
|
||||
await vscode.commands.executeCommand(
|
||||
'workbench.action.openEditorAtIndex',
|
||||
this.arguments.count - 1,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TabCommandType.Next:
|
||||
if (this.arguments.count !== undefined && this.arguments.count <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.arguments.count) {
|
||||
const tabGroup = vscode.window.tabGroups.activeTabGroup;
|
||||
if (0 < this.arguments.count && this.arguments.count <= tabGroup.tabs.length) {
|
||||
const tab = tabGroup.tabs[this.arguments.count - 1];
|
||||
if ((tab.input as vscode.TextDocument).uri !== undefined) {
|
||||
const uri = (tab.input as vscode.TextDocument).uri;
|
||||
await vscode.commands.executeCommand('vscode.open', uri);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await vscode.commands.executeCommand('workbench.action.nextEditorInGroup');
|
||||
}
|
||||
|
||||
break;
|
||||
case TabCommandType.Previous:
|
||||
if (this.arguments.count !== undefined && this.arguments.count <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
await this.executeCommandWithCount(
|
||||
this.arguments.count || 1,
|
||||
'workbench.action.previousEditorInGroup',
|
||||
);
|
||||
break;
|
||||
case TabCommandType.First:
|
||||
await vscode.commands.executeCommand('workbench.action.openEditorAtIndex1');
|
||||
break;
|
||||
case TabCommandType.Last:
|
||||
await vscode.commands.executeCommand('workbench.action.lastEditorInGroup');
|
||||
break;
|
||||
case TabCommandType.New: {
|
||||
const hasFile = !(this.arguments.file === undefined || this.arguments.file === '');
|
||||
if (hasFile) {
|
||||
const isAbsolute = path.isAbsolute(this.arguments.file!);
|
||||
const isInWorkspace =
|
||||
vscode.workspace.workspaceFolders !== undefined &&
|
||||
vscode.workspace.workspaceFolders.length > 0;
|
||||
const currentFilePath = vscode.window.activeTextEditor!.document.uri.fsPath;
|
||||
|
||||
let toOpenPath: string;
|
||||
if (isAbsolute) {
|
||||
toOpenPath = this.arguments.file!;
|
||||
} else if (isInWorkspace) {
|
||||
const workspacePath = vscode.workspace.workspaceFolders![0].uri.path;
|
||||
toOpenPath = path.join(workspacePath, this.arguments.file!);
|
||||
} else {
|
||||
toOpenPath = path.join(path.dirname(currentFilePath), this.arguments.file!);
|
||||
}
|
||||
|
||||
if (toOpenPath !== currentFilePath) {
|
||||
await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(toOpenPath));
|
||||
}
|
||||
} else {
|
||||
await vscode.commands.executeCommand('workbench.action.files.newUntitledFile');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TabCommandType.Close:
|
||||
// Navigate the correct position
|
||||
if (this.arguments.count === undefined) {
|
||||
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.arguments.count === 0) {
|
||||
// Wrong paramter
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: Close Page {count}. Page count is one-based.
|
||||
break;
|
||||
case TabCommandType.Only:
|
||||
await vscode.commands.executeCommand('workbench.action.closeOtherEditors');
|
||||
break;
|
||||
case TabCommandType.Move: {
|
||||
const { count, direction } = this.arguments;
|
||||
let args;
|
||||
if (direction !== undefined) {
|
||||
args = { to: direction, by: 'tab', value: count ?? 1 };
|
||||
} else if (count === 0) {
|
||||
args = { to: 'first' };
|
||||
} else if (count === undefined) {
|
||||
args = { to: 'last' };
|
||||
} else {
|
||||
args = { to: 'position', by: 'tab', value: count + 1 };
|
||||
}
|
||||
await vscode.commands.executeCommand('moveActiveEditor', args);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
12
src/cmd_line/commands/terminal.ts
Normal file
12
src/cmd_line/commands/terminal.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Parser, succeed } from 'parsimmon';
|
||||
import * as vscode from 'vscode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
|
||||
export class TerminalCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<TerminalCommand> = succeed(new TerminalCommand());
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
await vscode.commands.executeCommand('workbench.action.createTerminalEditor');
|
||||
}
|
||||
}
|
28
src/cmd_line/commands/undo.ts
Normal file
28
src/cmd_line/commands/undo.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { CommandUndo } from '../../actions/commands/actions';
|
||||
import { Position } from 'vscode';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { optWhitespace, Parser } from 'parsimmon';
|
||||
import { numberParser } from '../../vimscript/parserUtils';
|
||||
|
||||
//
|
||||
// Implements :u[ndo]
|
||||
// http://vimdoc.sourceforge.net/htmldoc/undo.html
|
||||
//
|
||||
export class UndoCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<UndoCommand> = optWhitespace
|
||||
.then(numberParser)
|
||||
.fallback(undefined)
|
||||
.map((count) => new UndoCommand(count));
|
||||
|
||||
private count?: number;
|
||||
private constructor(count?: number) {
|
||||
super();
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
// TODO: Use `this.count`
|
||||
await new CommandUndo().exec(new Position(0, 0), vimState);
|
||||
}
|
||||
}
|
27
src/cmd_line/commands/vscode.ts
Normal file
27
src/cmd_line/commands/vscode.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { ErrorCode, VimError } from '../../error';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import * as vscode from 'vscode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { all, Parser, whitespace } from 'parsimmon';
|
||||
|
||||
export class VsCodeCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<VsCodeCommand> = whitespace
|
||||
.then(all)
|
||||
.map((command) => new VsCodeCommand(command));
|
||||
|
||||
private command?: string;
|
||||
|
||||
public constructor(command?: string) {
|
||||
super();
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
if (!this.command) {
|
||||
StatusBar.displayError(vimState, VimError.fromCode(ErrorCode.ArgumentRequired));
|
||||
return;
|
||||
}
|
||||
await vscode.commands.executeCommand(this.command);
|
||||
}
|
||||
}
|
26
src/cmd_line/commands/wall.ts
Normal file
26
src/cmd_line/commands/wall.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Parser } from 'parsimmon';
|
||||
import * as vscode from 'vscode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { bangParser } from '../../vimscript/parserUtils';
|
||||
|
||||
//
|
||||
// Implements :wall (write all)
|
||||
// http://vimdoc.sourceforge.net/htmldoc/editing.html#:wall
|
||||
//
|
||||
export class WallCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<WallCommand> = bangParser.map(
|
||||
(bang) => new WallCommand(bang),
|
||||
);
|
||||
|
||||
private readonly bang: boolean;
|
||||
constructor(bang?: boolean) {
|
||||
super();
|
||||
this.bang = bang ?? false;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
// TODO : overwrite readonly files when bang? == true
|
||||
await vscode.workspace.saveAll(false);
|
||||
}
|
||||
}
|
160
src/cmd_line/commands/write.ts
Normal file
160
src/cmd_line/commands/write.ts
Normal file
@ -0,0 +1,160 @@
|
||||
// eslint-disable-next-line id-denylist
|
||||
import { all, alt, optWhitespace, Parser, seq, string } from 'parsimmon';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'platform/fs';
|
||||
import * as vscode from 'vscode';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { StatusBar } from '../../statusBar';
|
||||
import { Logger } from '../../util/logger';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { bangParser, fileNameParser, FileOpt, fileOptParser } from '../../vimscript/parserUtils';
|
||||
|
||||
export type IWriteCommandArguments = {
|
||||
bang: boolean;
|
||||
opt: FileOpt;
|
||||
bgWrite: boolean;
|
||||
file?: string;
|
||||
} & ({ cmd: string } | object);
|
||||
|
||||
//
|
||||
// Implements :write
|
||||
// http://vimdoc.sourceforge.net/htmldoc/editing.html#:write
|
||||
//
|
||||
export class WriteCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<WriteCommand> = seq(
|
||||
bangParser.skip(optWhitespace),
|
||||
fileOptParser.skip(optWhitespace),
|
||||
alt<{ cmd: string } | { file: string }>(
|
||||
string('!')
|
||||
.then(all)
|
||||
.map((cmd) => {
|
||||
return { cmd };
|
||||
}),
|
||||
fileNameParser.map((file) => {
|
||||
return { file };
|
||||
}),
|
||||
// TODO: Support `:help :w_a` ('>>')
|
||||
).fallback({}),
|
||||
).map(([bang, opt, other]) => new WriteCommand({ bang, opt, bgWrite: true, ...other }));
|
||||
|
||||
public override isRepeatableWithDot = false;
|
||||
|
||||
public readonly arguments: IWriteCommandArguments;
|
||||
|
||||
constructor(args: IWriteCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
// TODO: Use arguments: opt, file, cmd
|
||||
|
||||
// If the file isn't on disk because it's brand new or on a remote file system, let VS Code handle it
|
||||
if (vimState.document.isUntitled || vimState.document.uri.scheme !== 'file') {
|
||||
await this.background(vscode.commands.executeCommand('workbench.action.files.save'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.arguments.file) {
|
||||
await this.saveAs(vimState, this.arguments.file);
|
||||
} else {
|
||||
await fs.accessAsync(vimState.document.fileName, fs.constants.W_OK);
|
||||
await this.save(vimState);
|
||||
}
|
||||
} catch (accessErr) {
|
||||
if (this.arguments.bang) {
|
||||
try {
|
||||
const mode = await fs.getMode(vimState.document.fileName);
|
||||
await fs.chmodAsync(vimState.document.fileName, 0o666);
|
||||
// We must do a foreground write so we can await the save
|
||||
// and chmod the file back to its original state
|
||||
this.arguments.bgWrite = false;
|
||||
await this.save(vimState);
|
||||
await fs.chmodAsync(vimState.document.fileName, mode);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
StatusBar.setText(vimState, e.message);
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
StatusBar.setText(vimState, accessErr.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Aparentemente foi tudo, claro que falta alguns ERRORS e bla bla bla tipo o do :w 8/ E212 mas fds o E357 sobrepoe
|
||||
// TODO: fazer PR (#1876)
|
||||
private async saveAs(vimState: VimState, fileName: string): Promise<void> {
|
||||
try {
|
||||
const filePath = path.resolve(path.dirname(vimState.document.fileName), fileName);
|
||||
const fileExists = await fs.existsAsync(filePath);
|
||||
const uri = vscode.Uri.file(path.resolve(path.dirname(vimState.document.fileName), filePath));
|
||||
// An extension to the file must be specified.
|
||||
if (path.extname(filePath) === '') {
|
||||
StatusBar.setText(vimState, `E357: The file extension must be specified`, true);
|
||||
return;
|
||||
}
|
||||
// Checks if the file exists.
|
||||
if (fileExists) {
|
||||
const stats = await vscode.workspace.fs.stat(uri);
|
||||
const isDirectory = stats.type === vscode.FileType.Directory;
|
||||
// If it's a directory, throw an error.
|
||||
if (isDirectory) {
|
||||
StatusBar.setText(vimState, `E17: "${filePath}" is a directory`, true);
|
||||
return;
|
||||
}
|
||||
// Create a pop-up asking if user wants to overwrite the file.
|
||||
const confirmOverwrite = await vscode.window.showWarningMessage(
|
||||
`File "${fileName}" already exists. Do you want to overwrite it?`,
|
||||
{ modal: true },
|
||||
'Yes',
|
||||
'No',
|
||||
);
|
||||
|
||||
if (confirmOverwrite === 'No') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new file in 'filePath', appending the current's file content to it.
|
||||
await vscode.window.showTextDocument(vimState.document, { preview: false });
|
||||
await vscode.commands.executeCommand('workbench.action.files.save', uri);
|
||||
await vscode.workspace.fs.copy(vimState.document.uri, uri, { overwrite: true });
|
||||
|
||||
StatusBar.setText(
|
||||
vimState,
|
||||
`"${fileName}" ${fileExists ? '' : '[New]'} ${vimState.document.lineCount}L ${
|
||||
vimState.document.getText().length
|
||||
}C written`,
|
||||
);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
StatusBar.setText(vimState, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
private async save(vimState: VimState): Promise<void> {
|
||||
await this.background(
|
||||
vimState.document.save().then((success) => {
|
||||
if (success) {
|
||||
StatusBar.setText(
|
||||
vimState,
|
||||
`"${path.basename(vimState.document.fileName)}" ${vimState.document.lineCount}L ${
|
||||
vimState.document.getText().length
|
||||
}C written`,
|
||||
);
|
||||
} else {
|
||||
Logger.warn(':w failed');
|
||||
// TODO: What's the right thing to do here?
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async background<T>(fn: Thenable<T>): Promise<void> {
|
||||
if (!this.arguments.bgWrite) {
|
||||
await fn;
|
||||
}
|
||||
}
|
||||
}
|
42
src/cmd_line/commands/writequit.ts
Normal file
42
src/cmd_line/commands/writequit.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { optWhitespace, Parser, regexp, seq } from 'parsimmon';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { bangParser, fileNameParser, FileOpt, fileOptParser } from '../../vimscript/parserUtils';
|
||||
import { QuitCommand } from './quit';
|
||||
import { WriteCommand } from './write';
|
||||
|
||||
//
|
||||
// Implements :writequit
|
||||
// http://vimdoc.sourceforge.net/htmldoc/editing.html#write-quit
|
||||
//
|
||||
export interface IWriteQuitCommandArguments {
|
||||
bang: boolean;
|
||||
opt: FileOpt;
|
||||
file?: string;
|
||||
}
|
||||
|
||||
export class WriteQuitCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<WriteQuitCommand> = seq(
|
||||
bangParser.skip(optWhitespace),
|
||||
fileOptParser.skip(optWhitespace),
|
||||
fileNameParser.fallback(undefined),
|
||||
).map(([bang, opt, file]) => new WriteQuitCommand(file ? { bang, opt, file } : { bang, opt }));
|
||||
|
||||
public override isRepeatableWithDot = false;
|
||||
|
||||
private readonly args: IWriteQuitCommandArguments;
|
||||
constructor(args: IWriteQuitCommandArguments) {
|
||||
super();
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
// Writing command. Taken as a basis from the "write.ts" file.
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
await new WriteCommand({ bgWrite: false, ...this.args }).execute(vimState);
|
||||
|
||||
await new QuitCommand({
|
||||
// wq! fails when no file name is provided
|
||||
bang: false,
|
||||
}).execute(vimState);
|
||||
}
|
||||
}
|
47
src/cmd_line/commands/writequitall.ts
Normal file
47
src/cmd_line/commands/writequitall.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Parser, seq, whitespace } from 'parsimmon';
|
||||
import { VimState } from '../../state/vimState';
|
||||
import { ExCommand } from '../../vimscript/exCommand';
|
||||
import { bangParser, FileOpt, fileOptParser } from '../../vimscript/parserUtils';
|
||||
import * as wall from '../commands/wall';
|
||||
import * as quit from './quit';
|
||||
|
||||
//
|
||||
// Implements :writequitall
|
||||
// http://vimdoc.sourceforge.net/htmldoc/editing.html#:wqall
|
||||
//
|
||||
export interface IWriteQuitAllCommandArguments {
|
||||
bang: boolean;
|
||||
fileOpt: FileOpt;
|
||||
}
|
||||
|
||||
export class WriteQuitAllCommand extends ExCommand {
|
||||
public static readonly argParser: Parser<WriteQuitAllCommand> = seq(
|
||||
bangParser,
|
||||
whitespace.then(fileOptParser).fallback([]),
|
||||
).map(([bang, fileOpt]) => new WriteQuitAllCommand({ bang, fileOpt }));
|
||||
|
||||
public override isRepeatableWithDot = false;
|
||||
|
||||
private readonly arguments: IWriteQuitAllCommandArguments;
|
||||
constructor(args: IWriteQuitAllCommandArguments) {
|
||||
super();
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
// Writing command. Taken as a basis from the "write.ts" file.
|
||||
async execute(vimState: VimState): Promise<void> {
|
||||
const quitArgs: quit.IQuitCommandArguments = {
|
||||
// wq! fails when no file name is provided
|
||||
bang: false,
|
||||
};
|
||||
|
||||
const wallCmd = new wall.WallCommand(this.arguments.bang);
|
||||
await wallCmd.execute(vimState);
|
||||
|
||||
// TODO: fileOpt is not used
|
||||
|
||||
quitArgs.quitAll = true;
|
||||
const quitCmd = new quit.QuitCommand(quitArgs);
|
||||
await quitCmd.execute(vimState);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user