Skip to content

Commit 05c0c03

Browse files
gcsecseybgrgicak
authored andcommitted
User Settings: return the list of installed editors (#1217)
* Update install path detection logic * Update test types * Remove self-explanatory comments * Use `programfiles` env var for paths on Windows * Remove unnecessary log * restore linux supported apps * use path.win32 for windows path operations * fix mocking of process.env * tests: normalize mock paths This lets the tests running on Windows use UNIX style paths. * Return installed apps list in getInstalledApps * Return installed apps list in getInstalledApps * update types * Remove self-explanatory comments * Remove Nova, Sublime, and Atom from the list of editors We can add these back later as needed. * Remove Nova, Sublime, and Atom from the list of editors We can add these back later as needed.
1 parent 36a4967 commit 05c0c03

File tree

5 files changed

+223
-41
lines changed

5 files changed

+223
-41
lines changed

src/hooks/use-check-installed-apps.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { getIpcApi } from 'src/lib/get-ipc-api';
44
const initState = {
55
vscode: false,
66
phpstorm: false,
7+
webstorm: false,
8+
windsurf: false,
9+
cursor: false,
10+
iterm: false,
711
};
812
const checkInstalledAppsContext = createContext< InstalledApps >( initState );
913

src/ipc-handlers.ts

+3
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export async function getInstalledApps( _event: IpcMainInvokeEvent ): Promise< I
111111
return {
112112
vscode: isInstalled( 'vscode' ),
113113
phpstorm: isInstalled( 'phpstorm' ),
114+
webstorm: isInstalled( 'webstorm' ),
115+
windsurf: isInstalled( 'windsurf' ),
116+
cursor: isInstalled( 'cursor' ),
114117
};
115118
}
116119

src/ipc-types.d.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ interface StartedSiteDetails extends StoppedSiteDetails {
4646
type SiteDetails = StartedSiteDetails | StoppedSiteDetails;
4747

4848
type InstalledApps = {
49-
vscode: boolean | null;
50-
phpstorm: boolean | null;
49+
vscode: boolean;
50+
phpstorm: boolean;
51+
webstorm: boolean;
52+
windsurf: boolean;
53+
cursor: boolean;
5154
};
5255

5356
type InstalledTerminals = {

src/lib/is-installed.ts

+93-39
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,107 @@ import { app } from 'electron';
22
import fs from 'fs';
33
import path from 'path';
44

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

8-
if ( process.platform === 'darwin' ) {
9-
const systemApplications = '/Applications';
10-
const userApplications = path.join( app.getPath( 'home' ), 'Applications' );
9+
function getProgramFilesPath(): string {
10+
if ( process.platform !== 'win32' ) {
11+
return 'C:\\Program Files';
12+
}
13+
14+
// This env var dinamically points to the Program Files path
15+
// See https://stackoverflow.com/a/9608782
16+
const programFiles = process.env.ProgramFiles;
17+
if ( programFiles ) {
18+
return programFiles;
19+
}
1120

12-
appPaths = {
21+
// Fallback to default path if environment variable is not available
22+
return 'C:\\Program Files';
23+
}
24+
25+
// Define installation paths for each IDE by platform
26+
const installationPaths: Record< string, PlatformPaths > = {
27+
darwin: {
28+
vscode: [ 'Visual Studio Code.app' ],
29+
phpstorm: [ 'PhpStorm.app' ],
30+
cursor: [ 'Cursor.app' ],
31+
windsurf: [ 'Windsurf.app' ],
32+
webstorm: [ 'WebStorm.app' ],
33+
iterm: [ 'iTerm.app' ],
34+
terminal: [ 'Terminal.app' ],
35+
},
36+
linux: {
37+
vscode: [ '/usr/bin/code' ],
38+
phpstorm: [ '/usr/bin/phpstorm' ],
39+
cursor: [ '/usr/bin/cursor' ],
40+
windsurf: [ '/usr/bin/windsurf' ],
41+
webstorm: [ '/usr/bin/webstorm' ],
42+
iterm: [],
43+
terminal: [],
44+
},
45+
win32: {
1346
vscode: [
14-
path.join( systemApplications, 'Visual Studio Code.app' ),
15-
path.join( userApplications, 'Visual Studio Code.app' ),
47+
path.win32.join( getProgramFilesPath(), 'Microsoft VS Code' ),
48+
path.win32.join( app.getPath( 'appData' ), 'Local\\Programs\\Microsoft VS Code' ),
1649
],
1750
phpstorm: [
18-
path.join( systemApplications, 'PhpStorm.app' ),
19-
path.join( userApplications, 'PhpStorm.app' ),
51+
path.win32.join( getProgramFilesPath(), 'JetBrains\\PhpStorm' ),
52+
path.win32.join( app.getPath( 'appData' ), 'JetBrains\\PhpStorm' ),
2053
],
21-
};
22-
23-
terminalPaths = {
24-
iterm: [
25-
path.join( systemApplications, 'iTerm.app' ),
26-
path.join( userApplications, 'iTerm.app' ),
54+
cursor: [
55+
path.win32.join( getProgramFilesPath(), 'Cursor' ),
56+
path.win32.join( app.getPath( 'appData' ), 'Local\\Programs\\Cursor' ),
2757
],
28-
};
29-
} else if ( process.platform === 'linux' ) {
30-
appPaths = {
31-
vscode: [ '/usr/bin/code' ],
32-
phpstorm: [ '/usr/bin/phpstorm' ],
33-
};
34-
terminalPaths = {
35-
iterm: [], // iTerm is macOS only
36-
};
58+
windsurf: [
59+
path.win32.join( getProgramFilesPath(), 'Windsurf' ),
60+
path.win32.join( app.getPath( 'appData' ), 'Windsurf' ),
61+
],
62+
webstorm: [
63+
path.win32.join( getProgramFilesPath(), 'JetBrains\\WebStorm' ),
64+
path.win32.join( app.getPath( 'appData' ), 'JetBrains\\WebStorm' ),
65+
],
66+
iterm: [],
67+
terminal: [],
68+
},
69+
};
70+
71+
if ( process.platform === 'darwin' ) {
72+
const systemApplications = '/Applications';
73+
const userApplications = path.join( app.getPath( 'home' ), 'Applications' );
74+
75+
Object.keys( installationPaths.darwin ).forEach( ( ide ) => {
76+
const appName = installationPaths.darwin[ ide as keyof InstalledApps ][ 0 ];
77+
if ( appName ) {
78+
installationPaths.darwin[ ide as keyof InstalledApps ] = [
79+
path.join( systemApplications, appName ),
80+
path.join( userApplications, appName ),
81+
];
82+
}
83+
} );
3784
} else if ( process.platform === 'win32' ) {
38-
appPaths = {
39-
vscode: [ path.join( app.getPath( 'appData' ), 'Code' ) ],
40-
phpstorm: [ '' ], // Disable phpStorm for Windows
41-
};
42-
terminalPaths = {
43-
iterm: [], // iTerm is macOS only
44-
};
85+
// For JetBrains IDEs, check for version-specific folders
86+
[ 'phpstorm', 'webstorm' ].forEach( ( ide ) => {
87+
const basePaths = installationPaths.win32[ ide as keyof InstalledApps ];
88+
const jetbrainsDir = path.win32.join( getProgramFilesPath(), 'JetBrains' );
89+
90+
if ( fs.existsSync( jetbrainsDir ) ) {
91+
const entries = fs.readdirSync( jetbrainsDir );
92+
93+
entries.forEach( ( entry ) => {
94+
if ( entry.toLowerCase().includes( ide ) ) {
95+
basePaths.push( path.win32.join( jetbrainsDir, entry ) );
96+
}
97+
} );
98+
}
99+
} );
45100
}
46101

47-
export function isInstalled( key: keyof typeof appPaths | keyof typeof terminalPaths ): boolean {
48-
const paths =
49-
appPaths[ key as keyof typeof appPaths ] || terminalPaths[ key as keyof typeof terminalPaths ];
50-
if ( ! paths ) {
51-
return false;
52-
}
53-
return paths.some( ( path: string ) => path && fs.existsSync( path ) );
102+
export function isInstalled( key: keyof InstalledApps | keyof InstalledTerminals ): boolean {
103+
const platform = process.platform;
104+
const paths = installationPaths[ platform ]?.[ key ];
105+
106+
// Return true if any of the possible paths exist
107+
return paths.some( ( pathStr: string ) => pathStr && fs.existsSync( pathStr ) );
54108
}

src/lib/tests/is-installed.test.ts

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
import { app } from 'electron';
5+
import fs from 'fs';
6+
7+
jest.mock( 'fs', () => ( {
8+
existsSync: jest.fn(),
9+
readdirSync: jest.fn(),
10+
} ) );
11+
12+
jest.mock( 'electron', () => ( {
13+
app: {
14+
getPath: jest.fn(),
15+
},
16+
} ) );
17+
18+
describe( 'isInstalled', () => {
19+
let isInstalled: ( key: string ) => boolean;
20+
let mockPaths: string[];
21+
22+
beforeEach( () => {
23+
jest.resetAllMocks();
24+
mockPaths = [];
25+
26+
( fs.existsSync as jest.Mock ).mockImplementation( ( testPath: string ) => {
27+
const normalizedTestPath = testPath.replace( /\\/g, '/' );
28+
const normalizedMockPaths = mockPaths.map( ( p ) => p.replace( /\\/g, '/' ) );
29+
return normalizedMockPaths.includes( normalizedTestPath );
30+
} );
31+
32+
( app.getPath as jest.Mock ).mockImplementation( ( name: string ) => {
33+
switch ( name ) {
34+
case 'home':
35+
return '/mock/home/path';
36+
case 'appData':
37+
return process.platform === 'win32' ? 'C:\\mock\\AppData' : '/mock/home/path/.config';
38+
default:
39+
return '';
40+
}
41+
} );
42+
} );
43+
44+
describe( 'on macOS (darwin)', () => {
45+
beforeEach( () => {
46+
Object.defineProperty( process, 'platform', { value: 'darwin' } );
47+
// Re-import the module to ensure platform-specific paths are set up
48+
jest.isolateModules( () => {
49+
const module = require( '../is-installed' );
50+
isInstalled = module.isInstalled;
51+
} );
52+
} );
53+
54+
it( 'detects VS Code installed in system Applications', () => {
55+
mockPaths = [ '/Applications/Visual Studio Code.app' ];
56+
expect( isInstalled( 'vscode' ) ).toBe( true );
57+
} );
58+
59+
it( 'detects VS Code installed in user Applications', () => {
60+
mockPaths = [ '/mock/home/path/Applications/Visual Studio Code.app' ];
61+
expect( isInstalled( 'vscode' ) ).toBe( true );
62+
} );
63+
64+
it( 'returns false when VS Code is not installed', () => {
65+
mockPaths = [];
66+
expect( isInstalled( 'vscode' ) ).toBe( false );
67+
} );
68+
69+
it( 'detects PhpStorm installed', () => {
70+
mockPaths = [ '/Applications/PhpStorm.app' ];
71+
expect( isInstalled( 'phpstorm' ) ).toBe( true );
72+
} );
73+
} );
74+
75+
describe( 'on Windows (win32)', () => {
76+
beforeEach( () => {
77+
Object.defineProperty( process, 'platform', { value: 'win32' } );
78+
process.env.ProgramFiles = 'D:\\Program Files';
79+
// Re-import the module after setting the environment variable
80+
jest.isolateModules( () => {
81+
const module = require( '../is-installed' );
82+
isInstalled = module.isInstalled;
83+
} );
84+
} );
85+
86+
it( 'detects VS Code installed in Program Files', () => {
87+
mockPaths = [ 'D:\\Program Files\\Microsoft VS Code' ];
88+
89+
expect( isInstalled( 'vscode' ) ).toBe( true );
90+
} );
91+
92+
it( 'detects VS Code installed in AppData', () => {
93+
mockPaths = [ 'C:\\mock\\AppData\\Local\\Programs\\Microsoft VS Code' ];
94+
expect( isInstalled( 'vscode' ) ).toBe( true );
95+
} );
96+
97+
it( 'detects PhpStorm with version-specific folder', () => {
98+
mockPaths = [ 'D:\\Program Files\\JetBrains', 'D:\\Program Files\\JetBrains\\PhpStorm' ];
99+
100+
( fs.readdirSync as jest.Mock ).mockReturnValue( [ 'PhpStorm 2023.1', 'WebStorm 2023.1' ] );
101+
102+
expect( isInstalled( 'phpstorm' ) ).toBe( true );
103+
} );
104+
105+
it( 'falls back to default Program Files path when environment variable is not set', () => {
106+
delete process.env.ProgramFiles;
107+
108+
// Re-import the module after setting the environment variable
109+
jest.isolateModules( () => {
110+
const module = require( '../is-installed' );
111+
isInstalled = module.isInstalled;
112+
} );
113+
114+
mockPaths = [ 'C:\\Program Files\\Microsoft VS Code' ];
115+
expect( isInstalled( 'vscode' ) ).toBe( true );
116+
} );
117+
} );
118+
} );

0 commit comments

Comments
 (0)