Skip to content

Commit 55edd96

Browse files
Expose cursor position and add tests to check cursor position when cycling through multi line commands in command history
1 parent ac10064 commit 55edd96

File tree

5 files changed

+151
-0
lines changed

5 files changed

+151
-0
lines changed

src/commandwindow/CommandWindow.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,26 @@ export default class CommandWindow implements vscode.Pseudoterminal {
900900
this._justTypedLastInColumn = this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns === 0;
901901
}
902902

903+
/**
904+
* Get cursor position information for testing purposes.
905+
* Returns the logical line number (0-based) and column position (0-based) within that line.
906+
* For multi-line commands with explicit newlines, the line is determined by counting newlines.
907+
*/
908+
getCursorPosition (): { line: number, column: number } {
909+
const textUpToCursor = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(this._cursorIndex));
910+
const lastNewlineIndex = textUpToCursor.lastIndexOf('\n');
911+
912+
// Count newlines to determine line number
913+
const line = (textUpToCursor.match(/\n/g) ?? []).length;
914+
915+
// Calculate column position within the current line
916+
const column = lastNewlineIndex === -1
917+
? this._cursorIndex // No newlines, so cursor is on first line
918+
: textUpToCursor.length - lastNewlineIndex - 1 - this._currentPrompt.length;
919+
920+
return { line, column };
921+
}
922+
903923
onDidWrite: vscode.Event<string>;
904924
onDidOverrideDimensions?: vscode.Event<vscode.TerminalDimensions | undefined> | undefined;
905925
onDidClose?: vscode.Event<number> | undefined;

src/commandwindow/TerminalService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ export default class TerminalService {
9999
getCommandWindow (): CommandWindow {
100100
return this._commandWindow;
101101
}
102+
103+
/**
104+
* Get cursor position information for testing purposes.
105+
* Returns the logical line number (0-based) and column position (0-based) within that line.
106+
*/
107+
getCursorPosition (): { line: number, column: number } {
108+
return this._commandWindow.getCursorPosition();
109+
}
102110
}
103111

104112
/**

src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export async function activate (context: vscode.ExtensionContext): Promise<void>
162162
context.subscriptions.push(vscode.commands.registerCommand('matlab.runSelection', async () => await executionCommandProvider.handleRunSelection()))
163163
context.subscriptions.push(vscode.commands.registerCommand('matlab.interrupt', () => executionCommandProvider.handleInterrupt()))
164164
context.subscriptions.push(vscode.commands.registerCommand('matlab.openCommandWindow', async () => await terminalService.openTerminalOrBringToFront()))
165+
context.subscriptions.push(vscode.commands.registerCommand('matlab.getCursorPosition', () => terminalService.getCursorPosition()))
165166
context.subscriptions.push(vscode.commands.registerCommand('matlab.addFolderToPath', async (uri: vscode.Uri) => await executionCommandProvider.handleAddFolderToPath(uri)))
166167
context.subscriptions.push(vscode.commands.registerCommand('matlab.addFolderAndSubfoldersToPath', async (uri: vscode.Uri) => await executionCommandProvider.handleAddFolderAndSubfoldersToPath(uri)))
167168
context.subscriptions.push(vscode.commands.registerCommand('matlab.changeDirectory', async (uri: vscode.Uri) => await executionCommandProvider.handleChangeDirectory(uri)))

src/test/tools/tester/TerminalTester.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,32 @@ export class TerminalTester {
7575
const container = await this.terminal.findElement(vet.By.className('xterm-helper-textarea'));
7676
return await container.sendKeys(text)
7777
}
78+
79+
/**
80+
* Get the current cursor position in the MATLAB terminal
81+
* @returns The cursor position as { line: number, column: number } (both 0-based)
82+
*/
83+
public async getCursorPosition (): Promise<{ line: number, column: number }> {
84+
const workbench = new vet.Workbench()
85+
const position = await workbench.executeCommand('matlab.getCursorPosition')
86+
return position as { line: number, column: number }
87+
}
88+
89+
/**
90+
* Assert that the cursor is at the expected position
91+
* @param expectedLine Expected line number (0-based)
92+
* @param expectedColumn Expected column number (0-based)
93+
* @param message Message to display if assertion fails
94+
*/
95+
public async assertCursorPosition (expectedLine: number, expectedColumn: number, message: string): Promise<void> {
96+
return await this.vs.poll(
97+
async () => await this.getCursorPosition(),
98+
{ line: expectedLine, column: expectedColumn },
99+
`Assertion on cursor position: ${message}`,
100+
5000,
101+
async (result) => {
102+
console.log(`Expected cursor at line ${expectedLine}, column ${expectedColumn}, but got line ${result.line}, column ${result.column}`)
103+
}
104+
)
105+
}
78106
}

src/test/ui/terminal.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,98 @@ suite('Terminal UI Tests', () => {
112112
await vs.terminal.assertNotContains('3 4]', 'Second line should not stick after cycling away with down arrow')
113113
await vs.terminal.type(Key.ESCAPE)
114114
});
115+
116+
test('Test multi-line command cursor position', async () => {
117+
// Execute a multi-line command
118+
await vs.terminal.type('a = 1\nb = 2')
119+
await vs.terminal.type(Key.RETURN)
120+
await vs.terminal.executeCommand('clc')
121+
122+
// Recall the multi-line command
123+
await vs.terminal.type(Key.ARROW_UP)
124+
125+
// Cursor should be at the end of the command - verify position of cursor
126+
// Should be on line 1 (second line), at column 5 (after "b = 2")
127+
await vs.terminal.assertCursorPosition(1, 5, 'Cursor should be at end of multi-line command')
128+
await vs.terminal.type(Key.ESCAPE)
129+
});
130+
131+
test('Test multi-line command left arrow navigation to upper lines', async () => {
132+
// Execute a multi-line command
133+
await vs.terminal.type('x = 10\ny = 20\nz = 30')
134+
await vs.terminal.type(Key.RETURN)
135+
await vs.terminal.executeCommand('clc')
136+
137+
// Recall the multi-line command
138+
await vs.terminal.type(Key.ARROW_UP)
139+
140+
// Move left to navigate from last line to first line
141+
// Start at end: "z = 30|"
142+
for (let i = 0; i < 6; i++) {
143+
await vs.terminal.type(Key.ARROW_LEFT)
144+
}
145+
// Now at: "z = 30" -> should cross newline to second line
146+
await vs.terminal.type(Key.ARROW_LEFT)
147+
148+
// Verify we're on second line at the end
149+
// Should be on line 1 (second line), at column 6 (after "y = 20")
150+
await vs.terminal.assertCursorPosition(1, 6, 'Cursor should be at end of second line after navigating left from third line')
151+
await vs.terminal.type(Key.ESCAPE)
152+
});
153+
154+
test('Test multi-line command right arrow navigation to lower lines', async () => {
155+
// Execute a multi-line command
156+
await vs.terminal.type('p = 1\nq = 2')
157+
await vs.terminal.type(Key.RETURN)
158+
await vs.terminal.executeCommand('clc')
159+
160+
// Recall the multi-line command and navigate to start
161+
await vs.terminal.type(Key.ARROW_UP)
162+
await vs.terminal.type(Key.HOME)
163+
164+
// Now at start of first line: "|p = 1"
165+
// Move right to end of first line
166+
for (let i = 0; i < 5; i++) {
167+
await vs.terminal.type(Key.ARROW_RIGHT)
168+
}
169+
170+
// Now at: "p = 1|" -> next right should cross newline to second line
171+
await vs.terminal.type(Key.ARROW_RIGHT)
172+
173+
// Verify we're on second line at the beginning
174+
// Should be on line 1 (second line), at column 0 (start of "q = 2")
175+
await vs.terminal.assertCursorPosition(1, 0, 'Cursor should be at start of second line after navigating right from first line')
176+
await vs.terminal.type(Key.ESCAPE)
177+
});
178+
179+
test('Test multi-line command bidirectional navigation', async () => {
180+
// Execute a three-line command
181+
await vs.terminal.type('line1\nline2\nline3')
182+
await vs.terminal.type(Key.RETURN)
183+
await vs.terminal.executeCommand('clc')
184+
185+
// Recall and navigate: end -> line2 -> line1 -> line2 -> line3
186+
await vs.terminal.type(Key.ARROW_UP)
187+
188+
// Navigate to middle of second line using left arrows
189+
for (let i = 0; i < 8; i++) { // "line3" (5 chars) + newline + "li" (2 chars) = 8 left arrows
190+
await vs.terminal.type(Key.ARROW_LEFT)
191+
}
192+
193+
// Verify position on line2
194+
// Should be on line 1 (second line), at column 2 (after "li")
195+
await vs.terminal.assertCursorPosition(1, 2, 'Cursor should be at position 2 on line 1 after navigating left')
196+
197+
// Navigate back right to line3
198+
await vs.terminal.type(Key.ARROW_RIGHT) // move past 'n'
199+
await vs.terminal.type(Key.ARROW_RIGHT) // 'e'
200+
await vs.terminal.type(Key.ARROW_RIGHT) // '2'
201+
await vs.terminal.type(Key.ARROW_RIGHT) // cross newline to line3
202+
203+
// Verify we're back on line3
204+
// Should be on line 2 (third line), at column 0 (start of "line3")
205+
await vs.terminal.assertCursorPosition(2, 0, 'Cursor should be at start of line 2 after navigating right back')
206+
207+
await vs.terminal.type(Key.ESCAPE)
208+
});
115209
});

0 commit comments

Comments
 (0)