diff --git a/.eslintrc.json b/.eslintrc.json index 74e2d74..b53ab36 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -21,6 +21,7 @@ "@typescript-eslint/no-inferrable-types": "off", "@typescript-eslint/promise-function-async": "off", "@typescript-eslint/consistent-type-assertions": "off", + "object-shorthand": "off", "@typescript-eslint/indent": ["error", 4, { "SwitchCase": 1, "VariableDeclarator": 1, diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 30e59ba..def1e29 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -24,6 +24,7 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Useful Information** + - MATLAB Version: - OS Version: - VS Code Version: diff --git a/CHANGELOG.md b/CHANGELOG.md index e97b089..9357e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.3] - 2025-05-15 + +### Added +- Use automatic code completion to complete commands in the MATLAB terminal +- Move the cursor in the MATLAB terminal using `Alt+Click` +- Support for debugging P-coded files when the corresponding source file is available +- Filter the commands in the MATLAB terminal history by entering text in the terminal (Thanks @robertoffmoura!) + +### Fixed +- Resolves issues with the MATLAB workspace not updating correctly when switching contexts in the call stack +- Resolves potential crashes when using code completion in files without a .m file extension +- Patches CVE-2024-12905 + ## [1.3.2] - 2025-03-06 ### Fixed diff --git a/package-lock.json b/package-lock.json index 99d9159..f85c87c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "language-matlab", - "version": "1.3.2", + "version": "1.3.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "language-matlab", - "version": "1.3.2", + "version": "1.3.3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -6191,9 +6191,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "dev": true, "optional": true, "dependencies": { @@ -6277,9 +6277,9 @@ } }, "node_modules/targz/node_modules/tar-fs": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", - "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.4.tgz", + "integrity": "sha512-u3XczWoYAIVXe5GOKK6+VeWaHjtc47W7hyuTo3+4cNakcCcuDmlkYiiHEsECwTkcI3h1VUgtwBQ54+RvY6cM4w==", "dev": true, "dependencies": { "chownr": "^1.0.1", @@ -11828,9 +11828,9 @@ "dev": true }, "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "dev": true, "optional": true, "requires": { @@ -11907,9 +11907,9 @@ } }, "tar-fs": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", - "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.4.tgz", + "integrity": "sha512-u3XczWoYAIVXe5GOKK6+VeWaHjtc47W7hyuTo3+4cNakcCcuDmlkYiiHEsECwTkcI3h1VUgtwBQ54+RvY6cM4w==", "dev": true, "requires": { "chownr": "^1.0.1", diff --git a/package.json b/package.json index c396974..91cbc91 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Edit MATLAB code with syntax highlighting, linting, navigation support, and more", "icon": "public/L-Membrane_RGB_128x128.png", "license": "MIT", - "version": "1.3.2", + "version": "1.3.3", "engines": { "vscode": "^1.67.0" }, diff --git a/server b/server index 41d556f..afcef80 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 41d556fddba85735e3681d1ec52884f33d9e3b5e +Subproject commit afcef8062cbcf0205e75fef665bdad83cdba8d74 diff --git a/src/Notifications.ts b/src/Notifications.ts index 99dbba3..3cf9dd9 100644 --- a/src/Notifications.ts +++ b/src/Notifications.ts @@ -1,4 +1,4 @@ -// Copyright 2023-2024 The MathWorks, Inc. +// Copyright 2023-2025 The MathWorks, Inc. enum Notification { // Connection Status Updates @@ -15,6 +15,8 @@ enum Notification { // Execution MatlabRequestInstance = 'matlab/request', + TerminalCompletionRequest = 'TerminalCompletionRequest', + TerminalCompletionResponse = 'TerminalCompletionResponse', MVMEvalRequest = 'evalRequest', MVMEvalComplete = 'evalResponse', diff --git a/src/commandwindow/CommandWindow.ts b/src/commandwindow/CommandWindow.ts index 0188ada..d5b38ab 100644 --- a/src/commandwindow/CommandWindow.ts +++ b/src/commandwindow/CommandWindow.ts @@ -1,8 +1,11 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. import * as vscode from 'vscode' import { MVM, MatlabState } from './MVM' import { TextEvent, PromptState } from './MVMInterface' +import { createResolvablePromise, Notifier, ResolvablePromise } from './Utilities'; +import Notification from '../Notifications'; +import { CompletionList } from 'vscode-languageclient'; /** * Direction of cursor movement @@ -15,7 +18,7 @@ enum CursorDirection { /** * Direction of history movement */ -enum HistoryDirection { +enum Direction { BACKWARDS, FORWARDS } @@ -50,6 +53,8 @@ const ACTION_KEYS = { SELECT_ALL: '\x01', DELETE: ESC + '[3~', ESCAPE: ESC, + TAB: '\t', + SHIFT_TAB: ESC + '[Z', INVERT_COLORS: ESC + '[7m', RESTORE_COLORS: ESC + '[27m', @@ -66,6 +71,10 @@ const ACTION_KEYS = { QUERY_CURSOR: ESC + '[6n', SET_CURSOR_STYLE_TO_BAR: ESC + '[5 q' }; +// eslint-disable-next-line no-control-regex +const LEFT_REGEX = /^(\x1b\[D)+$/; +// eslint-disable-next-line no-control-regex +const RIGHT_REGEX = /^(\x1b\[C)+$/; const PROMPTS = { IDLE_PROMPT: '>> ', @@ -74,6 +83,13 @@ const PROMPTS = { BUSY_PROMPT: '' }; +// A modification of the word boundary regex being used by VS Code when replacing completions. +// The first part splits on numbers. The second/third parts split on quoted strings, ie. plot("Color"| +// the fourth part splits on unquoted words (same as VS Code's original regex), +// And the fifth part splits on unfinished quotes. ie. plot("C| +// eslint-disable-next-line no-useless-escape +const WORD_REGEX = /(-?\d*\.\d\w*)|(\"[^\"]*\"?)|(\'[^\']*\'?)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)|(\"|\')/ + /** * Represents command window. Is a pseudoterminal to be used as the input/output processor in a VS Code terminal. */ @@ -82,7 +98,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { private readonly _writeEmitter: vscode.EventEmitter; private _initialized: boolean = false; - private _currentPrompt = PROMPTS.IDLE_PROMPT; + private _currentPrompt: string = PROMPTS.IDLE_PROMPT; private _currentState: PromptState = PromptState.INITIALIZING; private _currentPromptLine: string = this._currentPrompt; @@ -101,12 +117,22 @@ export default class CommandWindow implements vscode.Pseudoterminal { private _justTypedLastInColumn: boolean = false; - constructor (mvm: MVM) { + private readonly _notifier: Notifier; + + private _latestTabCompletionData?: CompletionList; + private _currentCompletionIndex: number = -1; + private _pendingTabCompletionRequestNumber: number = -1; + private _pendingTabCompletionPromise?: ResolvablePromise; + + constructor (mvm: MVM, notifier: Notifier) { this._mvm = mvm; this._mvm.on(MVM.Events.output, this.addOutput.bind(this)); this._mvm.on(MVM.Events.clc, this.clear.bind(this)); this._mvm.on(MVM.Events.promptChange, this._handlePromptChange.bind(this)); + this._notifier = notifier; + this._notifier.onNotification(Notification.TerminalCompletionResponse, this._handleCompletionDataResponse.bind(this)); + this._initialized = false; this._writeEmitter = new vscode.EventEmitter(); @@ -248,6 +274,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._handleOutputLine(lines[i], i !== lines.length - 1); } } else { + this._invalidateCompletionData(); // Case 1: Normal typing if (lines.length === 1) { this._handleLine(lines[0]); @@ -300,8 +327,8 @@ export default class CommandWindow implements vscode.Pseudoterminal { return data.startsWith(ESC) || Object.values(ACTION_KEYS).includes(data) } - private _handleActionKeys (keyCode: string): boolean { - switch (keyCode) { + private _handleActionKeys (input: string): boolean { + switch (input) { case ACTION_KEYS.LEFT: return this._handleLeftRight(CursorDirection.LEFT, AnchorPolicy.MOVE); case ACTION_KEYS.RIGHT: @@ -321,9 +348,9 @@ export default class CommandWindow implements vscode.Pseudoterminal { case ACTION_KEYS.DELETE: return this._handleDelete(); case ACTION_KEYS.UP: - return this._handleNavigateHistory(HistoryDirection.BACKWARDS); + return this._handleNavigateHistory(Direction.BACKWARDS); case ACTION_KEYS.DOWN: - return this._handleNavigateHistory(HistoryDirection.FORWARDS); + return this._handleNavigateHistory(Direction.FORWARDS); case ACTION_KEYS.ESCAPE: return this._handleEscape(); case ACTION_KEYS.BACKSPACE: @@ -335,8 +362,26 @@ export default class CommandWindow implements vscode.Pseudoterminal { return this._handleCopy(); case ACTION_KEYS.PASTE: return this._handlePaste(); - default: - return false; + case ACTION_KEYS.TAB: + return this._handleTab(Direction.FORWARDS); + case ACTION_KEYS.SHIFT_TAB: + return this._handleTab(Direction.BACKWARDS); + default: { + let result = false; + // Handle repeated left/right arrow keys. This is what is received when using Alt+Mouse to move the cursor. + if (input.match(RIGHT_REGEX) != null) { + const count = input.length / 3; + for (let i = 0; i < count; i++) { + result ||= this._handleLeftRight(CursorDirection.RIGHT, AnchorPolicy.MOVE); + } + } else if (input.match(LEFT_REGEX) != null) { + const count = input.length / 3; + for (let i = 0; i < count; i++) { + result ||= this._handleLeftRight(CursorDirection.LEFT, AnchorPolicy.MOVE); + } + } + return result; + } } } @@ -346,12 +391,12 @@ export default class CommandWindow implements vscode.Pseudoterminal { return lines; } - private _handleNavigateHistory (direction: HistoryDirection): boolean { + private _handleNavigateHistory (direction: Direction): boolean { const isAtEnd = this._historyIndex === this._filteredCommandHistory.length; const isAtBeginning = this._historyIndex === 0; - if ((direction === HistoryDirection.BACKWARDS && isAtBeginning) || - (direction === HistoryDirection.FORWARDS && isAtEnd)) { + if ((direction === Direction.BACKWARDS && isAtBeginning) || + (direction === Direction.FORWARDS && isAtEnd)) { return false; } @@ -359,7 +404,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._lastKnownCurrentLine = this._stripCurrentPrompt(this._currentPromptLine); } - this._historyIndex += direction === HistoryDirection.BACKWARDS ? -1 : 1; + this._historyIndex += direction === Direction.BACKWARDS ? -1 : 1; const line = this._getHistoryItem(this._historyIndex); return this._replaceCurrentLineWithNewLine(this._currentPrompt + line); } @@ -441,6 +486,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._cursorIndex++; } + this._invalidateCompletionData(); return isLineDirty; } @@ -466,6 +512,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._currentPromptLine = before + after; this._cursorIndex--; this._markCurrentLineChanged(); + this._invalidateCompletionData(); return true; } @@ -473,6 +520,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._cursorIndex = this._getMaxIndexOnLine(); this._anchorIndex = 0; this._updateHasSelectionContext(); + this._invalidateCompletionData(); return true; } @@ -489,6 +537,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(this._cursorIndex) + 1); this._currentPromptLine = before + after; this._markCurrentLineChanged(); + this._invalidateCompletionData(); return true; } @@ -522,10 +571,10 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._writeEmitter.fire(ACTION_KEYS.CLEAR_AND_MOVE_TO_BEGINNING) } - private _replaceCurrentLineWithNewLine (updatedLine: string): boolean { + private _replaceCurrentLineWithNewLine (updatedLine: string, cursorIndex?: number): boolean { this._eraseExistingPromptLine(); this._currentPromptLine = updatedLine; - this._cursorIndex = this._getMaxIndexOnLine(); + this._cursorIndex = cursorIndex ?? this._getMaxIndexOnLine(); this._anchorIndex = undefined; this._updateWhetherJustTypedInLastColumn(); this._writeCurrentPromptLine(false); @@ -583,7 +632,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._updateHasSelectionContext(); this._lastKnownCurrentLine = this._stripCurrentPrompt(this._currentPromptLine); this._writeCurrentPromptLine(); - + this._invalidateCompletionData(); void this._evaluateCommand(stringToEvaluate); } @@ -677,6 +726,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { } private _handlePaste (): boolean { + this._invalidateCompletionData(); vscode.env.clipboard.readText().then((text: string) => { this.handleInput(text); }, () => { @@ -687,6 +737,109 @@ export default class CommandWindow implements vscode.Pseudoterminal { private _handleEscape (): boolean { this._setToEmptyPrompt(); + this._invalidateCompletionData(); + return true; + } + + private _requestCompletionData (code: string, offset: number): Promise { + this._invalidateCompletionData(); + this._pendingTabCompletionRequestNumber = this._pendingTabCompletionRequestNumber + 1; + this._notifier.sendNotification(Notification.TerminalCompletionRequest, { + requestId: this._pendingTabCompletionRequestNumber, + code: code, + offset: offset + }); + + this._pendingTabCompletionPromise = createResolvablePromise(); + return this._pendingTabCompletionPromise; + } + + private _handleCompletionDataResponse (data: any): void { + if (data.requestId === this._pendingTabCompletionRequestNumber && (this._pendingTabCompletionPromise != null)) { + this._pendingTabCompletionPromise.resolve(data.result); + } + } + + private _invalidateCompletionData (): void { + this._latestTabCompletionData = undefined; + this._pendingTabCompletionPromise?.reject(); + this._pendingTabCompletionPromise = undefined; + } + + private _doCompletion (): boolean { + if (this._latestTabCompletionData === undefined || this._latestTabCompletionData?.items.length === 0) { + return false; + } + + const currentCompletion = this._latestTabCompletionData.items[this._currentCompletionIndex].label; + const currentLine = this._stripCurrentPrompt(this._currentPromptLine); + + // Split the current line into words and non-words + const words = currentLine.split(WORD_REGEX).filter(match => match !== undefined && match !== ''); + const wordLengths = words.map(match => match.length); + const validWords = words.map(match => WORD_REGEX.test(match)); + validWords.unshift(false); + + // Find the first word/non-word the cursor is within + const cumulativeLengths = []; + let cumulativeLength = 0; + cumulativeLengths.push(0); + wordLengths.forEach((value) => { + cumulativeLength += value; + cumulativeLengths.push(cumulativeLength); + }); + + let i; + for (i = 0; i < cumulativeLengths.length; i++) { + if (this._cursorIndex <= cumulativeLengths[i]) { + break; + } + } + + if (i === cumulativeLengths.length) { + return false; + } + + // If the cursor is within or at the end of a valid word, then we want to replace that word. + if (validWords[i]) { + // Then get the code before the replacement and after the replacement + const codeBefore = currentLine.substring(0, cumulativeLengths[i - 1]); + const codeAfter = currentLine.substring(cumulativeLengths[i]); + // And construct the new line with the replacement made + const newLine = codeBefore + currentCompletion + codeAfter; + + this._replaceCurrentLineWithNewLine(this._currentPrompt + newLine, codeBefore.length + currentCompletion.length); + } else { + // Otherwise we want to just insert the new completion directly at the cursor. + const codeBefore = currentLine.substring(0, this._cursorIndex); + const codeAfter = currentLine.substring(this._cursorIndex); + // And construct the new line with the replacement made + const newLine = codeBefore + currentCompletion + codeAfter; + + this._replaceCurrentLineWithNewLine(this._currentPrompt + newLine, codeBefore.length + currentCompletion.length); + } + return true; + } + + private _handleTab (direction: Direction): boolean { + // If we have data and that not been invalidated, just increment the match index and do the replacement + if (this._latestTabCompletionData !== undefined) { + this._currentCompletionIndex = (this._currentCompletionIndex + this._latestTabCompletionData.items.length + (direction === Direction.FORWARDS ? 1 : -1)) % this._latestTabCompletionData.items.length; + return this._doCompletion(); + } else { + // Otherwise, request new completion data and do a completion when the data has come in. + const code = this._stripCurrentPrompt(this._currentPromptLine); + const offset = this._cursorIndex; + if (code.trim() === '') { + return false; + } + // If the request isn't invalidated before the data has arrived, then do the completion. Otherwise it will be rejected and ignored. + this._requestCompletionData(code, offset).then((completions: CompletionList) => { + this._latestTabCompletionData = completions; + this._currentCompletionIndex = 0; + this._doCompletion(); + }, () => { /* intentionally empty */ }); + } return true; } diff --git a/src/commandwindow/TerminalService.ts b/src/commandwindow/TerminalService.ts index 8abc634..8b1550e 100644 --- a/src/commandwindow/TerminalService.ts +++ b/src/commandwindow/TerminalService.ts @@ -1,4 +1,4 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. import * as vscode from 'vscode' import { MVM } from './MVM' @@ -23,7 +23,7 @@ export default class TerminalService { this._mvm = mvm; this._client = client; - this._commandWindow = new CommandWindow(mvm); + this._commandWindow = new CommandWindow(mvm, client); this._terminalOptions = { name: 'MATLAB', diff --git a/src/debug/MatlabDebugger.ts b/src/debug/MatlabDebugger.ts index 4658fc3..7b356e1 100644 --- a/src/debug/MatlabDebugger.ts +++ b/src/debug/MatlabDebugger.ts @@ -148,7 +148,7 @@ export default class MatlabDebugger { if ((vscode.debug as any).onDidChangeActiveStackItem !== undefined) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (vscode.debug as any).onDidChangeActiveStackItem((frame: any) => { - if (this._baseDebugSession !== null) { + if (this._baseDebugSession === null) { return; } diff --git a/src/test/tools/tester/TerminalTester.ts b/src/test/tools/tester/TerminalTester.ts index 6d4bc22..6102fe5 100644 --- a/src/test/tools/tester/TerminalTester.ts +++ b/src/test/tools/tester/TerminalTester.ts @@ -1,4 +1,4 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. import * as vet from 'vscode-extension-tester' import * as PollingUtils from '../utils/PollingUtils' @@ -55,4 +55,9 @@ export class TerminalTester { const content = await this.getTerminalContent() return content.includes(expected) } + + public async type (text: string): Promise { + const container = await this.terminal.findElement(vet.By.className('xterm-helper-textarea')); + return await container.sendKeys(text) + } } diff --git a/src/test/tools/tester/TestSuite.ts b/src/test/tools/tester/TestSuite.ts index 2b91d59..ac28876 100644 --- a/src/test/tools/tester/TestSuite.ts +++ b/src/test/tools/tester/TestSuite.ts @@ -36,7 +36,9 @@ export class TestSuite { 'window.dialogStyle': 'custom', 'terminal.integrated.copyOnSelection': true, 'debug.toolBarLocation': 'docked', - 'workbench.startupEditor': 'none' + 'workbench.startupEditor': 'none', + 'terminal.integrated.sendKeybindingsToShell': true, + 'editor.action.toggleTabFocusMode': false }) fs.writeFileSync(settingsjson, settings) this.vscodeSettings = settingsjson diff --git a/src/test/ui/terminal.test.ts b/src/test/ui/terminal.test.ts index 099c3be..6e9047f 100644 --- a/src/test/ui/terminal.test.ts +++ b/src/test/ui/terminal.test.ts @@ -1,6 +1,7 @@ // Copyright 2025 The MathWorks, Inc. import { VSCodeTester } from '../tools/tester/VSCodeTester' import { before, afterEach, after } from 'mocha'; +import { Key } from 'selenium-webdriver'; suite('Terminal Smoke Tests', () => { let vs: VSCodeTester @@ -14,7 +15,7 @@ suite('Terminal Smoke Tests', () => { }); afterEach(async () => { - await vs.terminal.executeCommand('clc'); + await vs.terminal.executeCommand('clc') }); after(async () => { @@ -35,4 +36,32 @@ suite('Terminal Smoke Tests', () => { await vs.terminal.executeCommand('clc'); await vs.terminal.assertContent('>>', 'clc should clear terminal') }) + + test('Test tab completions', async () => { + await vs.terminal.type('displ') + await vs.terminal.type(Key.TAB) + await vs.terminal.assertContains('display', 'terminal should contain display') + await vs.terminal.type(Key.ESCAPE) + await vs.terminal.assertContent('>>', 'ESCAPE should clear typed command and suggestion') + }) + + test('Test tab completions with context', async () => { + await vs.terminal.executeCommand('xVar = 1;') + await vs.terminal.type('disp(x') + await vs.terminal.type(Key.TAB) + await vs.terminal.assertContains('disp(xVar', 'terminal should contain disp(xVar') + await vs.terminal.type(Key.ESCAPE) + }) + + test('Test tab completions from within the command', async () => { + await vs.terminal.executeCommand('xVar = 1;') + await vs.terminal.type('x + 3') + await vs.terminal.type(Key.LEFT) + await vs.terminal.type(Key.LEFT) + await vs.terminal.type(Key.LEFT) + await vs.terminal.type(Key.LEFT) + await vs.terminal.type(Key.TAB) + await vs.terminal.assertContains('xVar + 3', 'terminal should contain xVar + 3') + await vs.terminal.type(Key.ESCAPE) + }) });