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 6 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
154 changes: 127 additions & 27 deletions src/lib/is-installed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,139 @@ import { app } from 'electron';
import fs from 'fs';
import path from 'path';

let appPaths: Record< keyof InstalledApps, string[] >;
type InstalledApp =
| 'vscode'
| 'phpstorm'
| 'cursor'
| 'windsurf'
| 'nova'
| 'webstorm'
| 'sublime'
| 'atom';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of notes regarding this list:

  • atom was archived in 2022
  • nova is mac-only


if ( process.platform === 'darwin' ) {
// Get both system and user Applications directories
const systemApplications = '/Applications';
const userApplications = path.join( app.getPath( 'home' ), 'Applications' );
type PlatformPaths = {
[ K in InstalledApp ]: string[];
};

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' ],
nova: [ 'Nova.app' ],
webstorm: [ 'WebStorm.app' ],
sublime: [ 'Sublime Text.app' ],
atom: [ 'Atom.app' ],
},
win32: {
vscode: [
path.join( systemApplications, 'Visual Studio Code.app' ),
path.join( userApplications, 'Visual Studio Code.app' ),
path.join( getProgramFilesPath(), 'Microsoft VS Code' ),
path.join( app.getPath( 'appData' ), 'Local\\Programs\\Microsoft VS Code' ),
],
phpstorm: [
path.join( systemApplications, 'PhpStorm.app' ),
path.join( userApplications, 'PhpStorm.app' ),
],
};
} else if ( process.platform === 'linux' ) {
appPaths = {
vscode: [ '/usr/bin/code' ],
phpstorm: [ '/usr/bin/phpstorm' ],
};
} else if ( process.platform === 'win32' ) {
appPaths = {
vscode: [ path.join( app.getPath( 'appData' ), 'Code' ) ],
phpstorm: [ '' ], // Disable phpStorm for Windows
};
path.join( getProgramFilesPath(), 'JetBrains\\PhpStorm' ),
path.join( app.getPath( 'appData' ), 'JetBrains\\PhpStorm' ),
],
cursor: [
path.join( getProgramFilesPath(), 'Cursor' ),
path.join( app.getPath( 'appData' ), 'Local\\Programs\\Cursor' ),
],
windsurf: [
path.join( getProgramFilesPath(), 'Windsurf' ),
path.join( app.getPath( 'appData' ), 'Windsurf' ),
],
nova: [], // Nova is Mac-only
webstorm: [
path.join( getProgramFilesPath(), 'JetBrains\\WebStorm' ),
path.join( app.getPath( 'appData' ), 'JetBrains\\WebStorm' ),
],
sublime: [
path.join( getProgramFilesPath(), 'Sublime Text' ),
path.join( getProgramFilesPath(), 'Sublime Text 3' ),
],
atom: [
path.join( app.getPath( 'appData' ), 'atom' ),
path.join( getProgramFilesPath(), 'Atom' ),
],
},
linux: {
vscode: [
'/usr/share/code',
path.join( app.getPath( 'home' ), '.local/share/code' ),
'/snap/code',
],
phpstorm: [ '/opt/phpstorm', path.join( app.getPath( 'home' ), 'PhpStorm' ) ],
cursor: [ '/opt/cursor', path.join( app.getPath( 'home' ), '.local/share/cursor' ) ],
windsurf: [ '/opt/windsurf', path.join( app.getPath( 'home' ), '.local/share/windsurf' ) ],
nova: [], // Nova is Mac-only
webstorm: [ '/opt/webstorm', path.join( app.getPath( 'home' ), 'WebStorm' ) ],
sublime: [ '/opt/sublime_text', '/usr/bin/sublime_text' ],
atom: [ '/usr/share/atom', path.join( app.getPath( 'home' ), '.atom' ) ],
},
};

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 InstalledApp ][ 0 ];
if ( appName ) {
installationPaths.darwin[ ide as InstalledApp ] = [
path.join( systemApplications, appName ),
path.join( userApplications, appName ),
];
}
} );
}

export function isInstalled( key: keyof typeof appPaths ): boolean {
if ( ! appPaths[ key ] ) {
return false;
}
if ( process.platform === 'linux' ) {
Object.keys( installationPaths.linux ).forEach( ( ide ) => {
installationPaths.linux[ ide as InstalledApp ].push( `/usr/bin/${ ide }` );
installationPaths.linux[ ide as InstalledApp ].push( `/usr/local/bin/${ ide }` );
} );
}

if ( process.platform === 'win32' ) {
// For JetBrains IDEs, check for version-specific folders
[ 'phpstorm', 'webstorm' ].forEach( ( ide ) => {
const basePaths = installationPaths.win32[ ide as InstalledApp ];
const jetbrainsDir = path.join( getProgramFilesPath(), 'JetBrains' );

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

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

export function isInstalled( key: InstalledApp ): boolean {
const platform = process.platform;
const paths = installationPaths[ platform ]?.[ key ] || [];

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

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

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

// Mock process.env for ProgramFiles
const originalEnv = process.env;
beforeAll( () => {
process.env = { ...originalEnv };
} );

afterAll( () => {
process.env = originalEnv;
} );

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

// Reset mocks before each test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's clean up the comments as they seem to be pretty self explanatory unless it is something that is not evident

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

// Mock fs.existsSync to check against our mockPaths array
( fs.existsSync as jest.Mock ).mockImplementation( ( testPath: string ) => {
return mockPaths.includes( testPath );
} );

// Mock app.getPath
( 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;
getProgramFilesPath = module.getProgramFilesPath;
} );
} );

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 );
} );

it( 'detects Nova installed (Mac-only)', () => {
mockPaths = [ '/Applications/Nova.app' ];
expect( isInstalled( 'nova' ) ).toBe( true );
} );
} );

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

it( 'detects VS Code installed in Program Files', () => {
// Set ProgramFiles environment variable
process.env.ProgramFiles = 'D:\\Program Files';

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

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

it( 'detects PhpStorm with version-specific folder', () => {
// Set ProgramFiles environment variable
process.env.ProgramFiles = 'E:\\Program Files';

const jetbrainsDir = 'E:\\Program Files\\JetBrains';
const versionSpecificPath = path.join( jetbrainsDir, 'PhpStorm 2023.1' );

mockPaths = [ jetbrainsDir, versionSpecificPath, 'E:\\Program Files\\JetBrains\\PhpStorm' ];

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

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

it( 'returns false for Nova on Windows (Mac-only)', () => {
mockPaths = [];
expect( isInstalled( 'nova' ) ).toBe( false );
} );

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

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

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

it( 'detects VS Code installed in /usr/share/code', () => {
mockPaths = [ '/usr/share/code' ];
expect( isInstalled( 'vscode' ) ).toBe( true );
} );

it( 'detects VS Code installed in user directory', () => {
mockPaths = [ path.join( '/mock/home/path', '.local/share/code' ) ];
expect( isInstalled( 'vscode' ) ).toBe( true );
} );

it( 'detects VS Code installed via snap', () => {
mockPaths = [ '/snap/code' ];
expect( isInstalled( 'vscode' ) ).toBe( true );
} );

it( 'detects VS Code installed as executable', () => {
mockPaths = [ '/usr/bin/vscode' ];
expect( isInstalled( 'vscode' ) ).toBe( true );
} );

it( 'returns false for Nova on Linux (Mac-only)', () => {
mockPaths = [];
expect( isInstalled( 'nova' ) ).toBe( false );
} );
} );
} );
Loading