refactor(ide ext): Update port file name + switch to 1-based index for characters + remove truncation text (#10501)

This commit is contained in:
Shreya Keshive
2025-12-12 12:39:15 -05:00
committed by GitHub
parent 12cbe320e4
commit 3b2a4ba27a
6 changed files with 88 additions and 156 deletions
@@ -6,7 +6,7 @@
import {
IdeDiffAcceptedNotificationSchema,
IdeDiffClosedNotificationSchema,
IdeDiffRejectedNotificationSchema,
} from '@google/gemini-cli-core/src/ide/types.js';
import { type JSONRPCNotification } from '@modelcontextprotocol/sdk/types.js';
import * as path from 'node:path';
@@ -134,7 +134,7 @@ export class DiffManager {
/**
* Closes an open diff view for a specific file.
*/
async closeDiff(filePath: string, suppressNotification = false) {
async closeDiff(filePath: string) {
let uriToClose: vscode.Uri | undefined;
for (const [uriString, diffInfo] of this.diffDocuments.entries()) {
if (diffInfo.originalFilePath === filePath) {
@@ -147,18 +147,6 @@ export class DiffManager {
const rightDoc = await vscode.workspace.openTextDocument(uriToClose);
const modifiedContent = rightDoc.getText();
await this.closeDiffEditor(uriToClose);
if (!suppressNotification) {
this.onDidChangeEmitter.fire(
IdeDiffClosedNotificationSchema.parse({
jsonrpc: '2.0',
method: 'ide/diffClosed',
params: {
filePath,
content: modifiedContent,
},
}),
);
}
return modifiedContent;
}
return;
@@ -204,9 +192,9 @@ export class DiffManager {
await this.closeDiffEditor(rightDocUri);
this.onDidChangeEmitter.fire(
IdeDiffClosedNotificationSchema.parse({
IdeDiffRejectedNotificationSchema.parse({
jsonrpc: '2.0',
method: 'ide/diffClosed',
method: 'ide/diffRejected',
params: {
filePath: diffInfo.originalFilePath,
content: modifiedContent,
@@ -27,6 +27,7 @@ vi.mock('node:fs/promises', () => ({
writeFile: vi.fn(() => Promise.resolve(undefined)),
unlink: vi.fn(() => Promise.resolve(undefined)),
chmod: vi.fn(() => Promise.resolve(undefined)),
mkdir: vi.fn(() => Promise.resolve(undefined)),
}));
vi.mock('node:os', async (importOriginal) => {
@@ -136,28 +137,23 @@ describe('IDEServer', () => {
const port = getPortFromMock(replaceMock);
const expectedPortFile = path.join(
'/tmp',
`gemini-ide-server-${port}.json`,
);
const expectedPpidPortFile = path.join(
'/tmp',
`gemini-ide-server-${process.ppid}.json`,
'gemini',
'ide',
`gemini-ide-server-${process.ppid}-${port}.json`,
);
const expectedContent = JSON.stringify({
port: parseInt(port, 10),
workspacePath: expectedWorkspacePaths,
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.mkdir).toHaveBeenCalledWith(path.join('/tmp', 'gemini', 'ide'), {
recursive: true,
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
expectedContent,
);
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
});
it('should set a single folder path', async () => {
@@ -174,28 +170,20 @@ describe('IDEServer', () => {
const port = getPortFromMock(replaceMock);
const expectedPortFile = path.join(
'/tmp',
`gemini-ide-server-${port}.json`,
);
const expectedPpidPortFile = path.join(
'/tmp',
`gemini-ide-server-${process.ppid}.json`,
'gemini',
'ide',
`gemini-ide-server-${process.ppid}-${port}.json`,
);
const expectedContent = JSON.stringify({
port: parseInt(port, 10),
workspacePath: '/foo/bar',
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
expectedContent,
);
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
});
it('should set an empty string if no folders are open', async () => {
@@ -212,28 +200,20 @@ describe('IDEServer', () => {
const port = getPortFromMock(replaceMock);
const expectedPortFile = path.join(
'/tmp',
`gemini-ide-server-${port}.json`,
);
const expectedPpidPortFile = path.join(
'/tmp',
`gemini-ide-server-${process.ppid}.json`,
'gemini',
'ide',
`gemini-ide-server-${process.ppid}-${port}.json`,
);
const expectedContent = JSON.stringify({
port: parseInt(port, 10),
workspacePath: '',
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
expectedContent,
);
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
});
it('should update the path when workspace folders change', async () => {
@@ -268,28 +248,20 @@ describe('IDEServer', () => {
const port = getPortFromMock(replaceMock);
const expectedPortFile = path.join(
'/tmp',
`gemini-ide-server-${port}.json`,
);
const expectedPpidPortFile = path.join(
'/tmp',
`gemini-ide-server-${process.ppid}.json`,
'gemini',
'ide',
`gemini-ide-server-${process.ppid}-${port}.json`,
);
const expectedContent = JSON.stringify({
port: parseInt(port, 10),
workspacePath: expectedWorkspacePaths,
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
expectedContent,
);
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
// Simulate removing a folder
vscodeMock.workspace.workspaceFolders = [{ uri: { fsPath: '/baz/qux' } }];
@@ -302,38 +274,31 @@ describe('IDEServer', () => {
const expectedContent2 = JSON.stringify({
port: parseInt(port, 10),
workspacePath: '/baz/qux',
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
expectedContent2,
);
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPpidPortFile,
expectedContent2,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
});
it('should clear env vars and delete port file on stop', async () => {
await ideServer.start(mockContext);
const replaceMock = mockContext.environmentVariableCollection.replace;
const port = getPortFromMock(replaceMock);
const portFile = path.join('/tmp', `gemini-ide-server-${port}.json`);
const ppidPortFile = path.join(
const portFile = path.join(
'/tmp',
`gemini-ide-server-${process.ppid}.json`,
'gemini',
'ide',
`gemini-ide-server-${process.ppid}-${port}.json`,
);
expect(fs.writeFile).toHaveBeenCalledWith(portFile, expect.any(String));
expect(fs.writeFile).toHaveBeenCalledWith(ppidPortFile, expect.any(String));
await ideServer.stop();
expect(mockContext.environmentVariableCollection.clear).toHaveBeenCalled();
expect(fs.unlink).toHaveBeenCalledWith(portFile);
expect(fs.unlink).toHaveBeenCalledWith(ppidPortFile);
});
it.skipIf(process.platform !== 'win32')(
@@ -356,28 +321,20 @@ describe('IDEServer', () => {
const port = getPortFromMock(replaceMock);
const expectedPortFile = path.join(
'/tmp',
`gemini-ide-server-${port}.json`,
);
const expectedPpidPortFile = path.join(
'/tmp',
`gemini-ide-server-${process.ppid}.json`,
'gemini',
'ide',
`gemini-ide-server-${process.ppid}-${port}.json`,
);
const expectedContent = JSON.stringify({
port: parseInt(port, 10),
workspacePath: expectedWorkspacePaths,
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
expectedContent,
);
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
},
);
+31 -54
View File
@@ -43,9 +43,8 @@ const IDE_AUTH_TOKEN_ENV_VAR = 'GEMINI_CLI_IDE_AUTH_TOKEN';
interface WritePortAndWorkspaceArgs {
context: vscode.ExtensionContext;
port: number;
portFile: string;
ppidPortFile: string;
authToken: string;
portFile: string | undefined;
log: (message: string) => void;
}
@@ -53,7 +52,6 @@ async function writePortAndWorkspace({
context,
port,
portFile,
ppidPortFile,
authToken,
log,
}: WritePortAndWorkspaceArgs): Promise<void> {
@@ -76,23 +74,21 @@ async function writePortAndWorkspace({
authToken,
);
if (!portFile) {
log('Missing portFile, cannot write port and workspace info.');
return;
}
const content = JSON.stringify({
port,
workspacePath,
ppid: process.ppid,
authToken,
});
log(`Writing port file to: ${portFile}`);
log(`Writing ppid port file to: ${ppidPortFile}`);
try {
await Promise.all([
fs.writeFile(portFile, content).then(() => fs.chmod(portFile, 0o600)),
fs
.writeFile(ppidPortFile, content)
.then(() => fs.chmod(ppidPortFile, 0o600)),
]);
await fs.writeFile(portFile, content).then(() => fs.chmod(portFile, 0o600));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
log(`Failed to write port to file: ${message}`);
@@ -121,7 +117,7 @@ export class IDEServer {
private context: vscode.ExtensionContext | undefined;
private log: (message: string) => void;
private portFile: string | undefined;
private ppidPortFile: string | undefined;
private port: number | undefined;
private authToken: string | undefined;
private transports: { [sessionId: string]: StreamableHTTPServerTransport } =
@@ -344,26 +340,28 @@ export class IDEServer {
const address = (this.server as HTTPServer).address();
if (address && typeof address !== 'string') {
this.port = address.port;
this.portFile = path.join(
os.tmpdir(),
`gemini-ide-server-${this.port}.json`,
);
this.ppidPortFile = path.join(
os.tmpdir(),
`gemini-ide-server-${process.ppid}.json`,
);
this.log(`IDE server listening on http://127.0.0.1:${this.port}`);
if (this.authToken) {
await writePortAndWorkspace({
context,
port: this.port,
portFile: this.portFile,
ppidPortFile: this.ppidPortFile,
authToken: this.authToken,
log: this.log,
});
let portFile: string | undefined;
try {
const portDir = path.join(os.tmpdir(), 'gemini', 'ide');
await fs.mkdir(portDir, { recursive: true });
portFile = path.join(
portDir,
`gemini-ide-server-${process.ppid}-${this.port}.json`,
);
this.portFile = portFile;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.log(`Failed to create IDE port file: ${message}`);
}
await writePortAndWorkspace({
context,
port: this.port,
portFile: this.portFile,
authToken: this.authToken ?? '',
log: this.log,
});
}
resolve();
});
@@ -392,19 +390,11 @@ export class IDEServer {
}
async syncEnvVars(): Promise<void> {
if (
this.context &&
this.server &&
this.port &&
this.portFile &&
this.ppidPortFile &&
this.authToken
) {
if (this.context && this.server && this.port && this.authToken) {
await writePortAndWorkspace({
context: this.context,
port: this.port,
portFile: this.portFile,
ppidPortFile: this.ppidPortFile,
authToken: this.authToken,
log: this.log,
});
@@ -437,13 +427,6 @@ export class IDEServer {
// Ignore errors if the file doesn't exist.
}
}
if (this.ppidPortFile) {
try {
await fs.unlink(this.ppidPortFile);
} catch (_err) {
// Ignore errors if the file doesn't exist.
}
}
}
}
@@ -477,15 +460,9 @@ const createMcpServer = (
description: '(IDE Tool) Close an open diff view for a specific file.',
inputSchema: CloseDiffRequestSchema.shape,
},
async ({
filePath,
suppressNotification,
}: z.infer<typeof CloseDiffRequestSchema>) => {
async ({ filePath }: z.infer<typeof CloseDiffRequestSchema>) => {
log(`Received closeDiff request for filePath: ${filePath}`);
const content = await diffManager.closeDiff(
filePath,
suppressNotification,
);
const content = await diffManager.closeDiff(filePath);
const response = { content: content ?? undefined };
return {
content: [
@@ -316,7 +316,7 @@ describe('OpenFilesManager', () => {
await vi.advanceTimersByTimeAsync(100);
const file = manager.state.workspaceState!.openFiles![0];
expect(file.cursor).toEqual({ line: 11, character: 20 });
expect(file.cursor).toEqual({ line: 11, character: 21 });
});
it('updates the selected text on selection change', async () => {
@@ -355,7 +355,7 @@ describe('OpenFilesManager', () => {
const manager = new OpenFilesManager(context);
const uri = getUri('/test/file1.txt');
const longText = 'a'.repeat(20000);
const truncatedText = longText.substring(0, 16384) + '... [TRUNCATED]';
const truncatedText = longText.substring(0, 16384);
const selection = {
active: { line: 10, character: 20 },
@@ -149,15 +149,14 @@ export class OpenFilesManager {
file.cursor = editor.selection.active
? {
line: editor.selection.active.line + 1,
character: editor.selection.active.character,
character: editor.selection.active.character + 1,
}
: undefined;
let selectedText: string | undefined =
editor.document.getText(editor.selection) || undefined;
if (selectedText && selectedText.length > MAX_SELECTED_TEXT_LENGTH) {
selectedText =
selectedText.substring(0, MAX_SELECTED_TEXT_LENGTH) + '... [TRUNCATED]';
selectedText = selectedText.substring(0, MAX_SELECTED_TEXT_LENGTH);
}
file.selectedText = selectedText;
}