55 DocumentFilter ,
66 languages ,
77 Range ,
8+ Selection ,
89 TextDocument ,
910 TextEdit ,
1011 TextEditor ,
@@ -22,6 +23,8 @@ import {
2223 ExtensionFormattingOptions ,
2324 ModuleResolverInterface ,
2425 PrettierBuiltInParserName ,
26+ PrettierCursorOptions ,
27+ PrettierCursorResult ,
2528 PrettierFileInfoResult ,
2629 PrettierInstance ,
2730 PrettierModule ,
@@ -104,6 +107,11 @@ interface ISelectors {
104107 languageSelector : ReadonlyArray < DocumentFilter > ;
105108}
106109
110+ interface FormatResult {
111+ formatted : string ;
112+ cursorOffset : number ;
113+ }
114+
107115export default class PrettierEditService implements Disposable {
108116 private formatterHandler : undefined | Disposable ;
109117 private rangeFormatterHandler : undefined | Disposable ;
@@ -448,21 +456,58 @@ export default class PrettierEditService implements Disposable {
448456 options : ExtensionFormattingOptions ,
449457 ) : Promise < TextEdit [ ] > => {
450458 const startTime = new Date ( ) . getTime ( ) ;
451- const result = await this . format ( document . getText ( ) , document , options ) ;
459+
460+ // Get cursor offset from active editor if available and not doing range formatting
461+ const editor = window . activeTextEditor ;
462+ const isRangeFormatting =
463+ options . rangeStart !== undefined && options . rangeEnd !== undefined ;
464+ let cursorOffset : number | undefined ;
465+
466+ if ( editor && editor . document === document && ! isRangeFormatting ) {
467+ cursorOffset = document . offsetAt ( editor . selection . active ) ;
468+ }
469+
470+ const result = await this . format (
471+ document . getText ( ) ,
472+ document ,
473+ options ,
474+ cursorOffset ,
475+ ) ;
452476 if ( ! result ) {
453477 // No edits happened, return never so VS Code can try other formatters
454478 return [ ] ;
455479 }
456480 const duration = new Date ( ) . getTime ( ) - startTime ;
457481 this . loggingService . logInfo ( `Formatting completed in ${ duration } ms.` ) ;
458- const edit = this . minimalEdit ( document , result ) ;
482+ const edit = this . minimalEdit ( document , result . formatted ) ;
459483 if ( ! edit ) {
460484 // Document is already formatted, no changes needed
461485 this . loggingService . logDebug (
462486 "Document is already formatted, no changes needed." ,
463487 ) ;
464488 return [ ] ;
465489 }
490+
491+ // Schedule cursor repositioning after VS Code applies the edit
492+ // We use setImmediate to run after the current event loop completes
493+ if (
494+ editor &&
495+ editor . document === document &&
496+ ! isRangeFormatting &&
497+ result . cursorOffset >= 0
498+ ) {
499+ setImmediate ( ( ) => {
500+ // Verify the editor is still active and document hasn't changed
501+ if (
502+ window . activeTextEditor === editor &&
503+ editor . document === document
504+ ) {
505+ const newPosition = document . positionAt ( result . cursorOffset ) ;
506+ editor . selection = new Selection ( newPosition , newPosition ) ;
507+ }
508+ } ) ;
509+ }
510+
466511 return [ edit ] ;
467512 } ;
468513
@@ -505,14 +550,17 @@ export default class PrettierEditService implements Disposable {
505550 /**
506551 * Format the given text with user's configuration.
507552 * @param text Text to format
508- * @param path formatting file's path
509- * @returns {string } formatted text
553+ * @param doc TextDocument being formatted
554+ * @param options Formatting options
555+ * @param cursorOffset Optional cursor offset for cursor preservation
556+ * @returns FormatResult with formatted text and new cursor offset, or undefined if formatting failed
510557 */
511558 private async format (
512559 text : string ,
513560 doc : TextDocument ,
514561 options : ExtensionFormattingOptions ,
515- ) : Promise < string | undefined > {
562+ cursorOffset ?: number ,
563+ ) : Promise < FormatResult | undefined > {
516564 const { fileName, uri, languageId } = doc ;
517565
518566 this . loggingService . logInfo ( `Formatting ${ uri } ` ) ;
@@ -631,18 +679,44 @@ export default class PrettierEditService implements Disposable {
631679 this . loggingService . logInfo ( "Prettier Options:" , prettierOptions ) ;
632680
633681 try {
682+ // Use formatWithCursor if we have a cursor offset to preserve cursor position
683+ if ( cursorOffset !== undefined ) {
684+ const cursorOptions : PrettierCursorOptions = {
685+ ...prettierOptions ,
686+ cursorOffset,
687+ } ;
688+
689+ // Check if formatWithCursor is available (it should be for all modern Prettier versions)
690+ if ( "formatWithCursor" in prettierInstance ) {
691+ const result : PrettierCursorResult =
692+ await prettierInstance . formatWithCursor ( text , cursorOptions ) ;
693+ this . statusBar . update ( FormatterStatus . Success ) ;
694+ return {
695+ formatted : result . formatted ,
696+ cursorOffset : result . cursorOffset ,
697+ } ;
698+ }
699+ }
700+
701+ // Fallback to regular format() if no cursor offset or formatWithCursor not available
634702 const formattedText = await prettierInstance . format (
635703 text ,
636704 prettierOptions ,
637705 ) ;
638706 this . statusBar . update ( FormatterStatus . Success ) ;
639707
640- return formattedText ;
708+ return {
709+ formatted : formattedText ,
710+ cursorOffset : - 1 , // Indicate that cursor position is unknown
711+ } ;
641712 } catch ( error ) {
642713 this . loggingService . logError ( "Error formatting document." , error ) ;
643714 this . statusBar . update ( FormatterStatus . Error ) ;
644715
645- return text ;
716+ return {
717+ formatted : text ,
718+ cursorOffset : cursorOffset ?? - 1 ,
719+ } ;
646720 }
647721 }
648722
0 commit comments