diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 039fe4f9e5b..19df7ae4f00 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,6 +12,11 @@ "type": "gulp", "task": "default", "problemMatcher": "$tsc-watch" + }, + { + "type": "gulp", + "task": "build", + "problemMatcher": [] } ] } \ No newline at end of file diff --git a/src/actions/commands/actions.ts b/src/actions/commands/actions.ts index 014d13d16ca..b1b8d9183ad 100644 --- a/src/actions/commands/actions.ts +++ b/src/actions/commands/actions.ts @@ -1,4 +1,7 @@ import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; import { RecordedState } from '../../state/recordedState'; import { ReplaceState } from '../../state/replaceState'; @@ -24,6 +27,9 @@ import { BaseAction } from './../base'; import { commandLine } from './../../cmd_line/commandLine'; import * as operator from './../operator'; import { Jump } from '../../jumps/jump'; +import { commandParsers } from '../../cmd_line/subparser'; +import { StatusBar } from '../../statusBar'; +import { GetAbsolutePath } from '../../util/path'; import { ReportLinesChanged, ReportClear, @@ -540,6 +546,13 @@ class CommandEsc extends BaseCommand { vimState.cursors = vimState.cursors.map(x => x.withNewStop(x.stop.getLeft())); } + if (vimState.currentMode === ModeName.SearchInProgressMode) { + vimState.statusBarCursorCharacterPos = 0; + if (vimState.globalState.searchState) { + vimState.cursorStopPosition = vimState.globalState.searchState.searchCursorStartPosition; + } + } + if (vimState.currentMode === ModeName.Normal && vimState.isMultiCursor) { vimState.isMultiCursor = false; } @@ -889,7 +902,14 @@ class CommandInsertInSearchMode extends BaseCommand { if (searchState.searchString.length === 0) { return new CommandEscInSearchMode().exec(position, vimState); } - searchState.searchString = searchState.searchString.slice(0, -1); + if (vimState.statusBarCursorCharacterPos === 0) { + return vimState; + } + + searchState.searchString = + searchState.searchString.slice(0, vimState.statusBarCursorCharacterPos - 1) + + searchState.searchString.slice(vimState.statusBarCursorCharacterPos); + vimState.statusBarCursorCharacterPos = Math.max(vimState.statusBarCursorCharacterPos - 1, 0); } else if (key === '\n') { await vimState.setCurrentMode(vimState.globalState.searchState!.previousMode); @@ -906,6 +926,7 @@ class CommandInsertInSearchMode extends BaseCommand { const nextMatch = searchState.getNextSearchMatchPosition(vimState.cursorStopPosition); vimState.cursorStopPosition = nextMatch.pos; + vimState.statusBarCursorCharacterPos = 0; Register.putByKey(searchState.searchString, '/', undefined, true); ReportSearch(nextMatch.index, searchState.matchRanges.length, vimState); @@ -920,6 +941,7 @@ class CommandInsertInSearchMode extends BaseCommand { if (prevSearchList[vimState.globalState.searchStateIndex] !== undefined) { searchState.searchString = prevSearchList[vimState.globalState.searchStateIndex].searchString; + vimState.statusBarCursorCharacterPos = searchState.searchString.length; } } else if (key === '') { vimState.globalState.searchStateIndex += 1; @@ -938,8 +960,12 @@ class CommandInsertInSearchMode extends BaseCommand { searchState.searchString = prevSearchList[vimState.globalState.searchStateIndex].searchString; } + vimState.statusBarCursorCharacterPos = searchState.searchString.length; } else { - searchState.searchString += this.keysPressed[0]; + let modifiedString = searchState.searchString.split(''); + modifiedString.splice(vimState.statusBarCursorCharacterPos, 0, key); + searchState.searchString = modifiedString.join(''); + vimState.statusBarCursorCharacterPos += key.length; } return vimState; @@ -965,6 +991,7 @@ class CommandEscInSearchMode extends BaseCommand { : undefined; await vimState.setCurrentMode(searchState.previousMode); + vimState.statusBarCursorCharacterPos = 0; return vimState; } @@ -1852,10 +1879,120 @@ class CommandShowCommandLine extends BaseCommand { } } +@RegisterAction +class CommandNavigateInCommandlineOrSearchMode extends BaseCommand { + modes = [ModeName.CommandlineInProgress, ModeName.SearchInProgressMode]; + keys = [[''], ['']]; + runsOnceForEveryCursor() { + return this.keysPressed[0] === '\n'; + } + + private getTrimmedStatusBarText() { + // first regex removes the : / and | from the string + // second regex removes a single space from the end of the string + let trimmedStatusBarText = StatusBar.Get() + .replace(/^(?:\/|\:)(.*)(?:\|)(.*)/, '$1$2') + .replace(/(.*) $/, '$1'); + return trimmedStatusBarText; + } + + public async exec(position: Position, vimState: VimState): Promise { + const key = this.keysPressed[0]; + const searchState = vimState.globalState.searchState!; + let statusBarText = this.getTrimmedStatusBarText(); + if (key === '') { + vimState.statusBarCursorCharacterPos = Math.min( + vimState.statusBarCursorCharacterPos + 1, + statusBarText.length + ); + } else if (key === '') { + vimState.statusBarCursorCharacterPos = Math.max(vimState.statusBarCursorCharacterPos - 1, 0); + } + return vimState; + } +} +@RegisterAction +class CommandTabInCommandline extends BaseCommand { + modes = [ModeName.CommandlineInProgress]; + keys = ['']; + runsOnceForEveryCursor() { + return this.keysPressed[0] === '\n'; + } + + private autoComplete(completionItems: any, vimState: VimState) { + if (commandLine.lastKeyPressed !== '') { + if (/ /g.test(vimState.currentCommandlineText)) { + // The regex here will match any text after the space or any text after the last / if it is present + const search = ( + /(?:.* .*\/|.* )(.*)/g.exec(vimState.currentCommandlineText) + ); + commandLine.autoCompleteText = search[1]; + commandLine.autoCompleteIndex = 0; + } else { + commandLine.autoCompleteText = vimState.currentCommandlineText; + commandLine.autoCompleteIndex = 0; + } + } + + completionItems = completionItems.filter(completionItem => + completionItem.startsWith(commandLine.autoCompleteText) + ); + + if ( + commandLine.lastKeyPressed === '' && + commandLine.autoCompleteIndex < completionItems.length + ) { + commandLine.autoCompleteIndex += 1; + } + if (commandLine.autoCompleteIndex >= completionItems.length) { + commandLine.autoCompleteIndex = 0; + } + + let result = completionItems[commandLine.autoCompleteIndex]; + if (result === vimState.currentCommandlineText) { + result = completionItems[++commandLine.autoCompleteIndex % completionItems.length]; + } + + if (result !== undefined && !/ /g.test(vimState.currentCommandlineText)) { + vimState.currentCommandlineText = result; + vimState.statusBarCursorCharacterPos = result.length; + } else if (result !== undefined) { + const searchArray = /(.* .*\/|.* )/g.exec(vimState.currentCommandlineText); + vimState.currentCommandlineText = searchArray[0] + result; + vimState.statusBarCursorCharacterPos = vimState.currentCommandlineText.length; + } + return vimState; + } + + public async exec(position: Position, vimState: VimState): Promise { + const key = this.keysPressed[0]; + + if (!/ /g.test(vimState.currentCommandlineText)) { + // Command completion + const commands = Object.keys(commandParsers).sort(); + vimState = this.autoComplete(commands, vimState); + } else { + // File Completion + let completeFiles: fs.Dirent[]; + const search = /.* (.*\/)/g.exec(vimState.currentCommandlineText); + let searchString = search !== null ? search[1] : ''; + + let fullPath = GetAbsolutePath(searchString); + + completeFiles = fs.readdirSync(fullPath, { withFileTypes: true }); + + vimState = this.autoComplete(completeFiles, vimState); + } + + commandLine.lastKeyPressed = key; + return vimState; + } +} + @RegisterAction class CommandInsertInCommandline extends BaseCommand { modes = [ModeName.CommandlineInProgress]; - keys = [[''], [''], [''], [''], [''], ['']]; + keys = [[''], [''], [''], ['']]; runsOnceForEveryCursor() { return this.keysPressed[0] === '\n'; } @@ -1911,20 +2048,14 @@ class CommandInsertInCommandline extends BaseCommand { } vimState.statusBarCursorCharacterPos = vimState.currentCommandlineText.length; - } else if (key === '') { - vimState.statusBarCursorCharacterPos = Math.min( - vimState.statusBarCursorCharacterPos + 1, - vimState.currentCommandlineText.length - ); - } else if (key === '') { - vimState.statusBarCursorCharacterPos = Math.max(vimState.statusBarCursorCharacterPos - 1, 0); - } else { + } else if (key !== '') { let modifiedString = vimState.currentCommandlineText.split(''); - modifiedString.splice(vimState.statusBarCursorCharacterPos, 0, this.keysPressed[0]); + modifiedString.splice(vimState.statusBarCursorCharacterPos, 0, key); vimState.currentCommandlineText = modifiedString.join(''); - vimState.statusBarCursorCharacterPos += 1; + vimState.statusBarCursorCharacterPos += key.length; } + commandLine.lastKeyPressed = key; return vimState; } } diff --git a/src/cmd_line/commandLine.ts b/src/cmd_line/commandLine.ts index 8186486b8ae..daa8fafbe56 100644 --- a/src/cmd_line/commandLine.ts +++ b/src/cmd_line/commandLine.ts @@ -19,6 +19,23 @@ class CommandLine { */ private _commandLineHistoryIndex: number = 0; + /** + * for checking the last pressed key in command mode + */ + public lastKeyPressed = ''; + + /** + * for checking the last pressed key in command mode + * + */ + public autoCompleteIndex = 0; + + /** + * for checking the last pressed key in command mode + * + */ + public autoCompleteText = ''; + public get commandlineHistoryIndex(): number { return this._commandLineHistoryIndex; } diff --git a/src/mode/modes.ts b/src/mode/modes.ts index 8725b687eba..231b36ac5d0 100644 --- a/src/mode/modes.ts +++ b/src/mode/modes.ts @@ -73,7 +73,11 @@ export class SearchInProgressMode extends Mode { } const leadingChar = vimState.globalState.searchState.searchDirection === SearchDirection.Forward ? '/' : '?'; - return `${leadingChar}${vimState.globalState.searchState!.searchString}`; + + let stringWithCursor = vimState.globalState.searchState!.searchString.split(''); + stringWithCursor.splice(vimState.statusBarCursorCharacterPos, 0, '|'); + + return `${leadingChar}${stringWithCursor.join('')}`; } getStatusBarCommandText(vimState: VimState): string { diff --git a/src/statusBar.ts b/src/statusBar.ts index ad20006ca05..085fba75f4a 100644 --- a/src/statusBar.ts +++ b/src/statusBar.ts @@ -55,6 +55,11 @@ class StatusBarImpl implements vscode.Disposable { this._statusBarItem.dispose(); } + public Get() { + let text = this._statusBarItem.text; + return text; + } + private UpdateText(text: string) { this._statusBarItem.text = text || ''; } diff --git a/src/util/path.ts b/src/util/path.ts new file mode 100644 index 00000000000..59afc2c2dc5 --- /dev/null +++ b/src/util/path.ts @@ -0,0 +1,28 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import untildify = require('untildify'); +import { parseTabOnlyCommandArgs } from '../cmd_line/subparsers/tab'; + +/** + * Given relative path, calculate absolute path. + */ +export function GetAbsolutePath(partialPath: string): string { + const editorFilePath = vscode.window.activeTextEditor!.document.uri.fsPath; + let basePath: string; + + if (partialPath.startsWith('/')) { + basePath = '/'; + } else if (partialPath.startsWith('~/')) { + basePath = untildify(partialPath); + partialPath = ''; + } else if (partialPath.startsWith('./')) { + basePath = path.dirname(editorFilePath); + partialPath = partialPath.replace('./', ''); + } else if (partialPath.startsWith('../')) { + basePath = path.dirname(editorFilePath) + '/'; + } else { + basePath = path.dirname(editorFilePath); + } + + return basePath + partialPath; +} diff --git a/test/cmd_line/cursorLocation.test.ts b/test/cmd_line/cursorLocation.test.ts new file mode 100644 index 00000000000..0965297a3a7 --- /dev/null +++ b/test/cmd_line/cursorLocation.test.ts @@ -0,0 +1,59 @@ +import * as vscode from 'vscode'; +import * as assert from 'assert'; + +import { getAndUpdateModeHandler } from '../../extension'; +import { commandLine } from '../../src/cmd_line/commandLine'; +import { ModeHandler } from '../../src/mode/modeHandler'; +import { createRandomFile, setupWorkspace, cleanUpWorkspace } from '../testUtils'; +import { StatusBar } from '../../src/statusBar'; + +suite('cursor location', () => { + let modeHandler: ModeHandler; + + suiteSetup(async () => { + await setupWorkspace(); + modeHandler = await getAndUpdateModeHandler(); + }); + + suiteTeardown(cleanUpWorkspace); + + test('cursor location in command line', async () => { + await modeHandler.handleMultipleKeyEvents([ + ':', + 't', + 'e', + 's', + 't', + '', + '', + '', + '', + ]); + + const statusBarAfterCursorMovement = StatusBar.Get(); + await modeHandler.handleKeyEvent(''); + + const statusBarAfterEsc = StatusBar.Get(); + assert.equal(statusBarAfterCursorMovement.trim(), ':tes|t', 'Command Tab Completion Failed'); + }); + + test('cursor location in search', async () => { + await modeHandler.handleMultipleKeyEvents([ + '/', + 't', + 'e', + 's', + 't', + '', + '', + '', + '', + ]); + + const statusBarAfterCursorMovement = StatusBar.Get(); + + await modeHandler.handleKeyEvent(''); + const statusBarAfterEsc = StatusBar.Get(); + assert.equal(statusBarAfterCursorMovement.trim(), '/tes|t', 'Command Tab Completion Failed'); + }); +}); diff --git a/test/cmd_line/tabCompletion.test.ts b/test/cmd_line/tabCompletion.test.ts new file mode 100644 index 00000000000..335276f38bc --- /dev/null +++ b/test/cmd_line/tabCompletion.test.ts @@ -0,0 +1,83 @@ +import * as vscode from 'vscode'; +import * as assert from 'assert'; + +import { getAndUpdateModeHandler } from '../../extension'; +import { commandLine } from '../../src/cmd_line/commandLine'; +import { ModeHandler } from '../../src/mode/modeHandler'; +import { createRandomFile, setupWorkspace, cleanUpWorkspace } from '../testUtils'; +import { StatusBar } from '../../src/statusBar'; + +suite('cmd_line tabComplete', () => { + let modeHandler: ModeHandler; + + suiteSetup(async () => { + await setupWorkspace(); + modeHandler = await getAndUpdateModeHandler(); + }); + + suiteTeardown(cleanUpWorkspace); + + test('command line command tab completion', async () => { + await modeHandler.handleMultipleKeyEvents([':', 'e', 'd', 'i']); + await modeHandler.handleKeyEvent(''); + const statusBarAfterTab = StatusBar.Get(); + + await modeHandler.handleKeyEvent(''); + assert.equal(statusBarAfterTab.trim(), ':edit|', 'Command Tab Completion Failed'); + }); + + test('command line file tab completion with no base path', async () => { + await modeHandler.handleKeyEvent(':'); + const statusBarBeforeTab = StatusBar.Get(); + + await modeHandler.handleMultipleKeyEvents(['e', ' ', '']); + const statusBarAfterTab = StatusBar.Get(); + + await modeHandler.handleKeyEvent(''); + assert.notEqual(statusBarBeforeTab, statusBarAfterTab, 'Status Bar did not change'); + }); + + test('command line file tab completion with / as base path', async () => { + await modeHandler.handleKeyEvent(':'); + const statusBarBeforeTab = StatusBar.Get(); + + await modeHandler.handleMultipleKeyEvents(['e', ' ', '.', '.', '/', '']); + const statusBarAfterTab = StatusBar.Get(); + + await modeHandler.handleKeyEvent(''); + assert.notEqual(statusBarBeforeTab, statusBarAfterTab, 'Status Bar did not change'); + }); + + test('command line file tab completion with ~/ as base path', async () => { + await modeHandler.handleKeyEvent(':'); + const statusBarBeforeTab = StatusBar.Get(); + + await modeHandler.handleMultipleKeyEvents(['e', ' ', '~', '/', '']); + const statusBarAfterTab = StatusBar.Get(); + + await modeHandler.handleKeyEvent(''); + assert.notEqual(statusBarBeforeTab, statusBarAfterTab, 'Status Bar did not change'); + }); + + test('command line file tab completion with ./ as base path', async () => { + await modeHandler.handleKeyEvent(':'); + const statusBarBeforeTab = StatusBar.Get(); + + await modeHandler.handleMultipleKeyEvents(['e', ' ', '.', '/', '']); + const statusBarAfterTab = StatusBar.Get(); + + await modeHandler.handleKeyEvent(''); + assert.notEqual(statusBarBeforeTab, statusBarAfterTab, 'Status Bar did not change'); + }); + + test('command line file tab completion with ../ as base path', async () => { + await modeHandler.handleKeyEvent(':'); + const statusBarBeforeTab = StatusBar.Get(); + + await modeHandler.handleMultipleKeyEvents(['e', ' ', '.', '.', '/', '']); + const statusBarAfterTab = StatusBar.Get(); + + await modeHandler.handleKeyEvent(''); + assert.notEqual(statusBarBeforeTab, statusBarAfterTab, 'Status Bar did not change'); + }); +});