@@ -15,6 +15,30 @@ import {
1515
1616import { detectFormattingOptions , writeJsonFile } from './json.ts' ;
1717
18+ type JsonEditorFile = {
19+ type : 'json' ;
20+ value : Record < string , unknown > ;
21+ } ;
22+
23+ type TextEditorFile = {
24+ type : 'text' ;
25+ value : string ;
26+ merge : ( originalText : string , incomingText : string ) => string ;
27+ } ;
28+
29+ type EditorFile = JsonEditorFile | TextEditorFile ;
30+
31+ function jsonEditorFile ( value : Record < string , unknown > ) : JsonEditorFile {
32+ return { type : 'json' , value } ;
33+ }
34+
35+ function textEditorFile (
36+ value : string ,
37+ merge : TextEditorFile [ 'merge' ] = ( _originalText , incomingText ) => incomingText ,
38+ ) : TextEditorFile {
39+ return { type : 'text' , value, merge } ;
40+ }
41+
1842// Language-specific overrides because user-level [lang] settings beat the workspace default
1943const VSCODE_LANGUAGE_OVERRIDES = {
2044 '[javascript]' : { 'editor.defaultFormatter' : 'oxc.oxc-vscode' } ,
@@ -150,22 +174,43 @@ const ZED_SETTINGS = {
150174 } ,
151175} as const ;
152176
177+ const JETBRAINS_OXC_PLUGIN_ID = 'com.github.oxc.project.oxcintellijplugin' ;
178+ const JETBRAINS_EXTERNAL_DEPENDENCIES = `<?xml version="1.0" encoding="UTF-8"?>
179+ <project version="4">
180+ <component name="ExternalDependencies">
181+ <plugin id="${ JETBRAINS_OXC_PLUGIN_ID } " />
182+ </component>
183+ </project>
184+ ` ;
185+
153186export const EDITORS = [
154187 {
155188 id : 'vscode' ,
156189 label : 'VSCode' ,
157190 targetDir : '.vscode' ,
158191 files : {
159- 'settings.json' : VSCODE_SETTINGS as Record < string , unknown > ,
160- 'extensions.json' : VSCODE_EXTENSIONS as Record < string , unknown > ,
192+ 'settings.json' : jsonEditorFile ( VSCODE_SETTINGS ) ,
193+ 'extensions.json' : jsonEditorFile ( VSCODE_EXTENSIONS ) ,
161194 } ,
162195 } ,
163196 {
164197 id : 'zed' ,
165198 label : 'Zed' ,
166199 targetDir : '.zed' ,
167200 files : {
168- 'settings.json' : ZED_SETTINGS as Record < string , unknown > ,
201+ 'settings.json' : jsonEditorFile ( ZED_SETTINGS ) ,
202+ } ,
203+ } ,
204+ {
205+ id : 'jetbrains' ,
206+ label : 'JetBrains' ,
207+ aliases : [ 'intellij' ] ,
208+ targetDir : '.idea' ,
209+ files : {
210+ 'externalDependencies.xml' : textEditorFile (
211+ JETBRAINS_EXTERNAL_DEPENDENCIES ,
212+ mergeJetBrainsExternalDependencies ,
213+ ) ,
169214 } ,
170215 } ,
171216] as const ;
@@ -383,8 +428,11 @@ async function writeEditorConfig({
383428
384429 for ( const [ fileName , baseIncoming ] of Object . entries ( editorConfig . files ) ) {
385430 const incoming =
386- editorId === 'vscode' && fileName === 'settings.json' && extraVsCodeSettings
387- ? { ...extraVsCodeSettings , ...baseIncoming }
431+ editorId === 'vscode' &&
432+ fileName === 'settings.json' &&
433+ extraVsCodeSettings &&
434+ baseIncoming . type === 'json'
435+ ? { ...baseIncoming , value : { ...extraVsCodeSettings , ...baseIncoming . value } }
388436 : baseIncoming ;
389437 const filePath = path . join ( targetDir , fileName ) ;
390438
@@ -434,7 +482,7 @@ async function writeEditorConfig({
434482 continue ;
435483 }
436484
437- writeJsonFile ( filePath , incoming ) ;
485+ writeEditorConfigFile ( filePath , incoming ) ;
438486 if ( ! silent ) {
439487 prompts . log . success ( `Wrote editor config to ${ editorConfig . targetDir } /${ fileName } ` ) ;
440488 }
@@ -448,6 +496,15 @@ function normalizeEditorSelection(editorId: EditorSelection): EditorId[] {
448496 return [ ...new Set ( Array . isArray ( editorId ) ? editorId : [ editorId ] ) ] ;
449497}
450498
499+ function writeEditorConfigFile ( filePath : string , file : EditorFile ) {
500+ if ( file . type === 'json' ) {
501+ writeJsonFile ( filePath , file . value ) ;
502+ return ;
503+ }
504+
505+ fs . writeFileSync ( filePath , file . value , 'utf-8' ) ;
506+ }
507+
451508/**
452509 * Merge incoming settings into an existing editor JSON/JSONC file by patching the
453510 * original text with `jsonc-parser` instead of re-serializing a merged object.
@@ -456,12 +513,28 @@ function normalizeEditorSelection(editorId: EditorSelection): EditorId[] {
456513 */
457514function mergeAndWriteEditorConfig (
458515 filePath : string ,
459- incoming : Record < string , unknown > ,
516+ incoming : EditorFile ,
460517 fileName : string ,
461518 displayPath : string ,
462519 silent = false ,
463520) {
464521 const originalText = fs . readFileSync ( filePath , 'utf-8' ) ;
522+ if ( incoming . type === 'text' ) {
523+ const newText = incoming . merge ( originalText , incoming . value ) ;
524+ if ( newText === originalText ) {
525+ if ( ! silent ) {
526+ prompts . log . info ( `No changes needed for ${ displayPath } ` ) ;
527+ }
528+ return ;
529+ }
530+
531+ fs . writeFileSync ( filePath , newText , 'utf-8' ) ;
532+ if ( ! silent ) {
533+ prompts . log . success ( `Merged editor config into ${ displayPath } ` ) ;
534+ }
535+ return ;
536+ }
537+
465538 const existing = parseJsonc ( originalText ) as unknown ;
466539 if ( ! isPlainObject ( existing ) ) {
467540 throw new Error ( `Cannot merge editor config: ${ displayPath } is not a JSON object` ) ;
@@ -470,8 +543,8 @@ function mergeAndWriteEditorConfig(
470543 const formattingOptions = detectFormattingOptions ( originalText ) ;
471544 const newText =
472545 fileName === 'extensions.json'
473- ? mergeExtensionsText ( originalText , existing , incoming , formattingOptions )
474- : mergeSettingsText ( originalText , existing , incoming , formattingOptions ) ;
546+ ? mergeExtensionsText ( originalText , existing , incoming . value , formattingOptions )
547+ : mergeSettingsText ( originalText , existing , incoming . value , formattingOptions ) ;
475548
476549 // Do not rewrite when the merge produced no changes (keeps the operation idempotent).
477550 if ( newText === originalText ) {
@@ -487,6 +560,55 @@ function mergeAndWriteEditorConfig(
487560 }
488561}
489562
563+ function mergeJetBrainsExternalDependencies ( originalText : string , incomingText : string ) : string {
564+ if ( hasJetBrainsPluginDependency ( originalText , JETBRAINS_OXC_PLUGIN_ID ) ) {
565+ return originalText ;
566+ }
567+
568+ const componentStart = originalText . search (
569+ / < c o m p o n e n t \s + n a m e = [ " ' ] E x t e r n a l D e p e n d e n c i e s [ " ' ] [ ^ > ] * > / ,
570+ ) ;
571+ if ( componentStart !== - 1 ) {
572+ const componentEnd = originalText . indexOf ( '</component>' , componentStart ) ;
573+ if ( componentEnd !== - 1 ) {
574+ const indentation = getLineIndentation ( originalText , componentEnd ) ;
575+ return insertAt (
576+ originalText ,
577+ componentEnd ,
578+ `${ indentation } <plugin id="${ JETBRAINS_OXC_PLUGIN_ID } " />\n` ,
579+ ) ;
580+ }
581+ }
582+
583+ const projectEnd = originalText . indexOf ( '</project>' ) ;
584+ if ( projectEnd !== - 1 ) {
585+ return insertAt (
586+ originalText ,
587+ projectEnd ,
588+ ` <component name="ExternalDependencies">\n <plugin id="${ JETBRAINS_OXC_PLUGIN_ID } " />\n </component>\n` ,
589+ ) ;
590+ }
591+
592+ return incomingText ;
593+ }
594+
595+ function hasJetBrainsPluginDependency ( text : string , pluginId : string ) : boolean {
596+ return new RegExp ( `<plugin\\s+[^>]*id=["']${ escapeRegExp ( pluginId ) } ["'][^>]*>` ) . test ( text ) ;
597+ }
598+
599+ function getLineIndentation ( text : string , index : number ) : string {
600+ const lineStart = text . lastIndexOf ( '\n' , index - 1 ) + 1 ;
601+ return text . slice ( lineStart , index ) . match ( / ^ \s * / ) ?. [ 0 ] ?? '' ;
602+ }
603+
604+ function insertAt ( text : string , index : number , value : string ) : string {
605+ return `${ text . slice ( 0 , index ) } ${ value } ${ text . slice ( index ) } ` ;
606+ }
607+
608+ function escapeRegExp ( value : string ) : string {
609+ return value . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
610+ }
611+
490612function isPlainObject ( value : unknown ) : value is Record < string , unknown > {
491613 return typeof value === 'object' && value !== null && ! Array . isArray ( value ) ;
492614}
@@ -576,7 +698,10 @@ function mergeExtensionsText(
576698function resolveEditorId ( editor : string ) : EditorId | undefined {
577699 const normalized = editor . trim ( ) . toLowerCase ( ) ;
578700 const match = EDITORS . find (
579- ( option ) => option . id === normalized || option . label . toLowerCase ( ) === normalized ,
701+ ( option ) =>
702+ option . id === normalized ||
703+ option . label . toLowerCase ( ) === normalized ||
704+ ( 'aliases' in option && ( option . aliases as readonly string [ ] ) . includes ( normalized ) ) ,
580705 ) ;
581706 return match ?. id ;
582707}
0 commit comments