mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-29 22:44:45 -07:00
refactor(ide ext): Update port file name + switch to 1-based index for characters + remove truncation text (#10501)
This commit is contained in:
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user