@@ -783,76 +783,151 @@ export class ProjectsController {
783783 }
784784
785785 /**
786- * Gets the default folder path for a given item type when creating at project root.
787- * For schema-dependent items: Checks Schema/ObjectType/ (e.g., Sales/Functions/)
788- * For non-schema items (like Database Trigger): Checks root-level ObjectType folder (e.g., DatabaseTriggers/)
786+ * Finds an existing folder in the project by path (case-insensitive). If it does not exist
787+ * and `autoCreate` is true, adds it. Otherwise returns `fallback`.
788+ */
789+ private async findOrAddFolder (
790+ project : ISqlProject ,
791+ targetPath : string ,
792+ fallback : string ,
793+ autoCreate : boolean ,
794+ ) : Promise < string > {
795+ const existing = project . folders . find (
796+ ( f ) => f . relativePath . toLowerCase ( ) === targetPath . toLowerCase ( ) ,
797+ ) ;
798+ if ( existing ) {
799+ return existing . relativePath ;
800+ }
801+ if ( autoCreate ) {
802+ await project . addFolder ( targetPath ) ;
803+ return targetPath ;
804+ }
805+ return fallback ;
806+ }
807+
808+ /**
809+ * Returns whether the auto-create folder structure setting is enabled.
810+ * Defaults to true if not set.
811+ */
812+ private isAutoCreateFoldersEnabled ( ) : boolean {
813+ return vscode . workspace
814+ . getConfiguration ( )
815+ . get < boolean > ( constants . autoCreateFoldersSetting , true ) ;
816+ }
817+
818+ /**
819+ * Resolves the folder path for the item being created.
820+ *
821+ * When called from the project root (basePath is empty):
822+ * - Schema-dependent items build `Schema/ObjectType` (e.g. `dbo/Tables`)
823+ * - Non-schema-dependent items build a root-level `ObjectType` folder (e.g. `DatabaseTriggers`)
824+ *
825+ * When called from an existing folder node (basePath is non-empty, e.g. user right-clicked `dbo`):
826+ * - Checks whether `basePath/ObjectTypeFolder` exists and uses it if so
827+ * - If it doesn't exist and auto-create is ON, creates `basePath/ObjectTypeFolder`
828+ * - If it doesn't exist and auto-create is OFF, returns `basePath` unchanged
829+ * - If the last path component already equals the ObjectTypeFolder name (user is already
830+ * inside e.g. `dbo/Tables`), returns `basePath` unchanged to avoid double-nesting
831+ *
789832 * @param itemType The type of item being created
790- * @param project The project to check for existing folders
791- * @param schemaName Optional schema name to look for schema-named folders
792- * @returns The default folder path if it exists, or empty string otherwise
833+ * @param project The project to check / add folders to
834+ * @param schemaName The schema name parsed from user input (e.g. "dbo", "sales")
835+ * @param basePath The folder the user invoked the command from; empty string means project root
836+ * @returns The relative folder path the item should be placed in
793837 */
794- public getDefaultFolderForItemType (
838+ public async resolveItemFolder (
795839 itemType : ItemType ,
796840 project : ISqlProject ,
797841 schemaName ?: string ,
798- ) : string {
799- let relativePath = "" ;
800-
801- // Get the folder config for this item type (defaults to schema-dependent if not in map)
842+ basePath ?: string ,
843+ ) : Promise < string > {
802844 const folderConfig = templates . itemTypeToFolderMap . get ( itemType ) ;
803- const isSchemaDependent = folderConfig ?. schemaDependent ?? true ;
804- const folderName = folderConfig ?. folderName ;
845+ if ( ! folderConfig ) {
846+ return basePath ?? "" ;
847+ }
805848
806- // Non-schema-dependent items - check root-level folder only
807- if ( ! isSchemaDependent && folderName ) {
808- const rootFolder = project . folders . find (
809- ( f ) => f . relativePath . toLowerCase ( ) === folderName . toLowerCase ( ) ,
849+ const { folderName, schemaDependent } = folderConfig ;
850+ const autoCreate = this . isAutoCreateFoldersEnabled ( ) ;
851+
852+ // Non-root path: user invoked from an existing folder node
853+ if ( basePath ) {
854+ // Use /[\\/]/ instead of path.sep: basePath comes from trimUri which always
855+ // joins with '/' on all platforms, so on Windows path.sep ('\') would never split "dbo/Tables"
856+ // correctly. The regex handles both separators defensively.
857+ const segments = basePath . split ( / [ \\ / ] / ) . filter ( Boolean ) ;
858+
859+ // Already inside a Schema/ObjectType folder (2+ segments, e.g. "dbo/Functions") —
860+ // place the file directly there; Tables and Functions are siblings, not nested.
861+ if ( segments . length >= 2 ) {
862+ return basePath ;
863+ }
864+
865+ // Non-schema-dependent types (e.g. DatabaseTriggers, Security) live at the project
866+ // root and must not be nested under a schema folder or themselves.
867+ if ( ! schemaDependent ) {
868+ return basePath ;
869+ }
870+
871+ // Short-circuit if basePath is already any known ObjectType folder
872+ // (e.g. user is in "Tables" and adds a View → don't create "Tables/Views").
873+ // Files must only be nested under schema folders (single-segment, non-ObjectType),
874+ // so if basePath matches any folderName in the map we place the file directly there.
875+ const isObjectTypeFolder = [ ...templates . itemTypeToFolderMap . values ( ) ] . some (
876+ ( cfg ) => cfg . folderName . toLowerCase ( ) === basePath . toLowerCase ( ) ,
810877 ) ;
811- if ( rootFolder ) {
812- relativePath = rootFolder . relativePath ;
878+ if ( isObjectTypeFolder ) {
879+ return basePath ;
813880 }
814- return relativePath ;
881+
882+ // basePath is a single-segment schema folder (e.g. "dbo").
883+ // Check for / create the ObjectType subfolder under it.
884+ const subfolderPath = utils . convertSlashesForSqlProj ( path . join ( basePath , folderName ) ) ;
885+ return this . findOrAddFolder ( project , subfolderPath , basePath , autoCreate ) ;
815886 }
816887
817- // Case for Sequence: Check root-level Sequences folder first
818- if ( itemType === ItemType . sequence && folderName ) {
819- const rootObjectFolder = project . folders . find (
888+ // Non-schema-dependent items (e.g. Security/, DatabaseTriggers/) → root-level folder
889+ if ( ! schemaDependent ) {
890+ return this . findOrAddFolder ( project , folderName , "" , autoCreate ) ;
891+ }
892+
893+ // Backward compat: if a root-level Sequences folder already exists, prefer it
894+ // over creating schema/Sequences (mirrors behavior on projects created before
895+ // the auto-create-folders feature where sequences lived at the project root).
896+ if ( itemType === ItemType . sequence ) {
897+ const rootSeqFolder = project . folders . find (
820898 ( f ) => f . relativePath . toLowerCase ( ) === folderName . toLowerCase ( ) ,
821899 ) ;
822-
823- if ( rootObjectFolder ) {
824- return rootObjectFolder . relativePath ;
900+ if ( rootSeqFolder ) {
901+ return rootSeqFolder . relativePath ;
825902 }
826903 }
827904
828- // For schema-dependent items, check schema folders
829- if ( schemaName ) {
830- // Case 1: Check for schema folder (e.g., "Sales", "dbo") - case-insensitive
831- const schemaFolder = project . folders . find (
832- ( f ) => f . relativePath . toLowerCase ( ) === schemaName . toLowerCase ( ) ,
833- ) ;
834-
835- if ( schemaFolder ) {
836- relativePath = schemaFolder . relativePath ;
905+ // Schema-dependent items (e.g. dbo/Tables/, sales/StoredProcedures/)
906+ const resolvedSchema = schemaName ?? constants . defaultSchemaName ;
907+ const nestedPath = utils . convertSlashesForSqlProj ( path . join ( resolvedSchema , folderName ) ) ;
837908
838- // Case 2: Check for nested object type folder (e.g., "Sales/Functions")
839- if ( folderName ) {
840- const nestedPath = utils . convertSlashesForSqlProj (
841- path . join ( schemaFolder . relativePath , folderName ) ,
842- ) ;
843- const nestedFolder = project . folders . find (
844- ( f ) => f . relativePath . toLowerCase ( ) === nestedPath . toLowerCase ( ) ,
845- ) ;
909+ // Single scan for nestedPath — avoids a second scan inside findOrAddFolder.
910+ const existingNested = project . folders . find (
911+ ( f ) => f . relativePath . toLowerCase ( ) === nestedPath . toLowerCase ( ) ,
912+ ) ;
913+ if ( existingNested ) {
914+ return existingNested . relativePath ;
915+ }
846916
847- if ( nestedFolder ) {
848- relativePath = nestedFolder . relativePath ;
849- }
850- }
851- }
917+ if ( ! autoCreate ) {
918+ return "" ;
852919 }
853920
854- // Case 3: If no schema folder found, return empty string (place at root)
855- return relativePath ;
921+ // Auto-create: ensure the parent schema folder exists before adding the nested path.
922+ if (
923+ ! project . folders . some (
924+ ( f ) => f . relativePath . toLowerCase ( ) === resolvedSchema . toLowerCase ( ) ,
925+ )
926+ ) {
927+ await project . addFolder ( resolvedSchema ) ;
928+ }
929+ await project . addFolder ( nestedPath ) ;
930+ return nestedPath ;
856931 }
857932
858933 public async addItemPromptFromNode (
@@ -931,11 +1006,16 @@ export class ProjectsController {
9311006 // Parse schema and object name from input (e.g., "sales.MyFunction" -> schema="sales", objectName="MyFunction")
9321007 const { schemaName, objectName } = this . parseSchemaAndObjectName ( itemObjectName ) ;
9331008
934- // Determine the folder for this item when creating at project root
935- // Checks: Schema folder -> Schema/ObjectType folder -> root
936- if ( relativePath === "" ) {
937- relativePath = this . getDefaultFolderForItemType ( itemType . type , project , schemaName ) ;
938- }
1009+ // Resolve the best folder for this item type, whether adding from root or from
1010+ // an existing folder node (e.g. user right-clicked the dbo/ schema folder).
1011+ // resolveItemFolder handles both cases: creates missing subfolders when auto-create
1012+ // is ON, or returns the existing folder / falls back to the current path when OFF.
1013+ relativePath = await this . resolveItemFolder (
1014+ itemType . type ,
1015+ project ,
1016+ schemaName ,
1017+ relativePath || undefined ,
1018+ ) ;
9391019
9401020 const relativeFilePath = path . join ( relativePath , objectName + fileExtension ) ;
9411021
0 commit comments