Skip to content

User Settings: return the list of installed editors #1217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/hooks/use-check-installed-apps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { getIpcApi } from 'src/lib/get-ipc-api';
const initState = {
vscode: false,
phpstorm: false,
webstorm: false,
windsurf: false,
cursor: false,
iterm: false,
};
const checkInstalledAppsContext = createContext< InstalledApps >( initState );

Expand Down
3 changes: 3 additions & 0 deletions src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ export async function getInstalledApps( _event: IpcMainInvokeEvent ): Promise< I
return {
vscode: isInstalled( 'vscode' ),
phpstorm: isInstalled( 'phpstorm' ),
webstorm: isInstalled( 'webstorm' ),
windsurf: isInstalled( 'windsurf' ),
cursor: isInstalled( 'cursor' ),
};
}

Expand Down
7 changes: 5 additions & 2 deletions src/ipc-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ interface Snapshot {
}

type InstalledApps = {
vscode: boolean | null;
phpstorm: boolean | null;
vscode: boolean;
phpstorm: boolean;
webstorm: boolean;
windsurf: boolean;
cursor: boolean;
};

type InstalledTerminals = {
Expand Down
132 changes: 93 additions & 39 deletions src/lib/is-installed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,107 @@ import { app } from 'electron';
import fs from 'fs';
import path from 'path';

let appPaths: Record< keyof InstalledApps, string[] >;
let terminalPaths: Record< 'iterm', string[] >;
type PlatformPaths = {
[ K in keyof InstalledApps | keyof InstalledTerminals ]: string[];
};

if ( process.platform === 'darwin' ) {
const systemApplications = '/Applications';
const userApplications = path.join( app.getPath( 'home' ), 'Applications' );
function getProgramFilesPath(): string {
if ( process.platform !== 'win32' ) {
return 'C:\\Program Files';
}

// This env var dinamically points to the Program Files path
// See https://stackoverflow.com/a/9608782
const programFiles = process.env.ProgramFiles;
if ( programFiles ) {
return programFiles;
}

appPaths = {
// Fallback to default path if environment variable is not available
return 'C:\\Program Files';
}

// Define installation paths for each IDE by platform
const installationPaths: Record< string, PlatformPaths > = {
darwin: {
vscode: [ 'Visual Studio Code.app' ],
phpstorm: [ 'PhpStorm.app' ],
cursor: [ 'Cursor.app' ],
windsurf: [ 'Windsurf.app' ],
webstorm: [ 'WebStorm.app' ],
iterm: [ 'iTerm.app' ],
terminal: [ 'Terminal.app' ],
},
linux: {
vscode: [ '/usr/bin/code' ],
phpstorm: [ '/usr/bin/phpstorm' ],
cursor: [ '/usr/bin/cursor' ],
windsurf: [ '/usr/bin/windsurf' ],
webstorm: [ '/usr/bin/webstorm' ],
iterm: [],
terminal: [],
},
win32: {
vscode: [
path.join( systemApplications, 'Visual Studio Code.app' ),
path.join( userApplications, 'Visual Studio Code.app' ),
path.win32.join( getProgramFilesPath(), 'Microsoft VS Code' ),
path.win32.join( app.getPath( 'appData' ), 'Local\\Programs\\Microsoft VS Code' ),
],
phpstorm: [
path.join( systemApplications, 'PhpStorm.app' ),
path.join( userApplications, 'PhpStorm.app' ),
path.win32.join( getProgramFilesPath(), 'JetBrains\\PhpStorm' ),
path.win32.join( app.getPath( 'appData' ), 'JetBrains\\PhpStorm' ),
],
};

terminalPaths = {
iterm: [
path.join( systemApplications, 'iTerm.app' ),
path.join( userApplications, 'iTerm.app' ),
cursor: [
path.win32.join( getProgramFilesPath(), 'Cursor' ),
path.win32.join( app.getPath( 'appData' ), 'Local\\Programs\\Cursor' ),
],
};
} else if ( process.platform === 'linux' ) {
appPaths = {
vscode: [ '/usr/bin/code' ],
phpstorm: [ '/usr/bin/phpstorm' ],
};
terminalPaths = {
iterm: [], // iTerm is macOS only
};
windsurf: [
path.win32.join( getProgramFilesPath(), 'Windsurf' ),
path.win32.join( app.getPath( 'appData' ), 'Windsurf' ),
],
webstorm: [
path.win32.join( getProgramFilesPath(), 'JetBrains\\WebStorm' ),
path.win32.join( app.getPath( 'appData' ), 'JetBrains\\WebStorm' ),
],
iterm: [],
terminal: [],
},
};

if ( process.platform === 'darwin' ) {
const systemApplications = '/Applications';
const userApplications = path.join( app.getPath( 'home' ), 'Applications' );

Object.keys( installationPaths.darwin ).forEach( ( ide ) => {
const appName = installationPaths.darwin[ ide as keyof InstalledApps ][ 0 ];
if ( appName ) {
installationPaths.darwin[ ide as keyof InstalledApps ] = [
path.join( systemApplications, appName ),
path.join( userApplications, appName ),
];
}
} );
} else if ( process.platform === 'win32' ) {
appPaths = {
vscode: [ path.join( app.getPath( 'appData' ), 'Code' ) ],
phpstorm: [ '' ], // Disable phpStorm for Windows
};
terminalPaths = {
iterm: [], // iTerm is macOS only
};
// For JetBrains IDEs, check for version-specific folders
[ 'phpstorm', 'webstorm' ].forEach( ( ide ) => {
const basePaths = installationPaths.win32[ ide as keyof InstalledApps ];
const jetbrainsDir = path.win32.join( getProgramFilesPath(), 'JetBrains' );

if ( fs.existsSync( jetbrainsDir ) ) {
const entries = fs.readdirSync( jetbrainsDir );

entries.forEach( ( entry ) => {
if ( entry.toLowerCase().includes( ide ) ) {
basePaths.push( path.win32.join( jetbrainsDir, entry ) );
}
} );
}
} );
}

export function isInstalled( key: keyof typeof appPaths | keyof typeof terminalPaths ): boolean {
const paths =
appPaths[ key as keyof typeof appPaths ] || terminalPaths[ key as keyof typeof terminalPaths ];
if ( ! paths ) {
return false;
}
return paths.some( ( path: string ) => path && fs.existsSync( path ) );
export function isInstalled( key: keyof InstalledApps | keyof InstalledTerminals ): boolean {
const platform = process.platform;
const paths = installationPaths[ platform ]?.[ key ];

// Return true if any of the possible paths exist
return paths.some( ( pathStr: string ) => pathStr && fs.existsSync( pathStr ) );
}
118 changes: 118 additions & 0 deletions src/lib/tests/is-installed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* @jest-environment node
*/
import { app } from 'electron';
import fs from 'fs';

jest.mock( 'fs', () => ( {
existsSync: jest.fn(),
readdirSync: jest.fn(),
} ) );

jest.mock( 'electron', () => ( {
app: {
getPath: jest.fn(),
},
} ) );

describe( 'isInstalled', () => {
let isInstalled: ( key: string ) => boolean;
let mockPaths: string[];

beforeEach( () => {
jest.resetAllMocks();
mockPaths = [];

( fs.existsSync as jest.Mock ).mockImplementation( ( testPath: string ) => {
const normalizedTestPath = testPath.replace( /\\/g, '/' );
const normalizedMockPaths = mockPaths.map( ( p ) => p.replace( /\\/g, '/' ) );
return normalizedMockPaths.includes( normalizedTestPath );
} );

( app.getPath as jest.Mock ).mockImplementation( ( name: string ) => {
switch ( name ) {
case 'home':
return '/mock/home/path';
case 'appData':
return process.platform === 'win32' ? 'C:\\mock\\AppData' : '/mock/home/path/.config';
default:
return '';
}
} );
} );

describe( 'on macOS (darwin)', () => {
beforeEach( () => {
Object.defineProperty( process, 'platform', { value: 'darwin' } );
// Re-import the module to ensure platform-specific paths are set up
jest.isolateModules( () => {
const module = require( '../is-installed' );
isInstalled = module.isInstalled;
} );
} );

it( 'detects VS Code installed in system Applications', () => {
mockPaths = [ '/Applications/Visual Studio Code.app' ];
expect( isInstalled( 'vscode' ) ).toBe( true );
} );

it( 'detects VS Code installed in user Applications', () => {
mockPaths = [ '/mock/home/path/Applications/Visual Studio Code.app' ];
expect( isInstalled( 'vscode' ) ).toBe( true );
} );

it( 'returns false when VS Code is not installed', () => {
mockPaths = [];
expect( isInstalled( 'vscode' ) ).toBe( false );
} );

it( 'detects PhpStorm installed', () => {
mockPaths = [ '/Applications/PhpStorm.app' ];
expect( isInstalled( 'phpstorm' ) ).toBe( true );
} );
} );

describe( 'on Windows (win32)', () => {
beforeEach( () => {
Object.defineProperty( process, 'platform', { value: 'win32' } );
process.env.ProgramFiles = 'D:\\Program Files';
// Re-import the module after setting the environment variable
jest.isolateModules( () => {
const module = require( '../is-installed' );
isInstalled = module.isInstalled;
} );
} );

it( 'detects VS Code installed in Program Files', () => {
mockPaths = [ 'D:\\Program Files\\Microsoft VS Code' ];

expect( isInstalled( 'vscode' ) ).toBe( true );
} );

it( 'detects VS Code installed in AppData', () => {
mockPaths = [ 'C:\\mock\\AppData\\Local\\Programs\\Microsoft VS Code' ];
expect( isInstalled( 'vscode' ) ).toBe( true );
} );

it( 'detects PhpStorm with version-specific folder', () => {
mockPaths = [ 'D:\\Program Files\\JetBrains', 'D:\\Program Files\\JetBrains\\PhpStorm' ];

( fs.readdirSync as jest.Mock ).mockReturnValue( [ 'PhpStorm 2023.1', 'WebStorm 2023.1' ] );

expect( isInstalled( 'phpstorm' ) ).toBe( true );
} );

it( 'falls back to default Program Files path when environment variable is not set', () => {
delete process.env.ProgramFiles;

// Re-import the module after setting the environment variable
jest.isolateModules( () => {
const module = require( '../is-installed' );
isInstalled = module.isInstalled;
} );

mockPaths = [ 'C:\\Program Files\\Microsoft VS Code' ];
expect( isInstalled( 'vscode' ) ).toBe( true );
} );
} );
} );
Loading