44 * ------------------------------------------------------------------------------------------ */
55
66import {
7- workspace as Workspace , languages as Languages , TextDocument , TextDocumentChangeEvent , TextDocumentWillSaveEvent , TextEdit as VTextEdit ,
8- DocumentSelector as VDocumentSelector , Event , EventEmitter , Disposable ,
9- workspace
7+ workspace as Workspace , languages as Languages , TextDocument , TextLine , TextDocumentChangeEvent , TextDocumentWillSaveEvent , TextEdit as VTextEdit ,
8+ DocumentSelector as VDocumentSelector , Event , EventEmitter , Disposable , Uri as VUri , workspace , type EndOfLine , Position as VPosition , Range as VRange
109} from 'vscode' ;
1110
1211import {
@@ -22,6 +21,7 @@ import {
2221} from './features' ;
2322
2423import * as UUID from './utils/uuid' ;
24+ import { TextDocument as TextDocumentImpl } from 'vscode-languageserver-textdocument' ;
2525
2626export interface TextDocumentSynchronizationMiddleware {
2727 didOpen ?: NextSignature < TextDocument , Promise < void > > ;
@@ -77,7 +77,14 @@ export class DidOpenTextDocumentFeature extends TextDocumentEventFeature<DidOpen
7777 if ( visibleDocuments . isVisible ( document ) ) {
7878 return super . callback ( document ) ;
7979 } else {
80- this . _pendingOpenNotifications . set ( document . uri . toString ( ) , document ) ;
80+ // Snapshot the text document so that when we send the delayed
81+ // notification it is based on the content/version at the time
82+ // it would've been sent, and not the updated version.
83+ //
84+ // See https://github.com/microsoft/vscode-languageserver-node/issues/1695
85+
86+ const snapshot = new TextDocumentSnapshot ( document ) ;
87+ this . _pendingOpenNotifications . set ( snapshot . uri . toString ( ) , snapshot ) ;
8188 }
8289 }
8390 }
@@ -632,4 +639,191 @@ export class DidSaveTextDocumentFeature extends TextDocumentEventFeature<DidSave
632639 protected getTextDocument ( data : TextDocument ) : TextDocument {
633640 return data ;
634641 }
642+ }
643+
644+ // Copied from https://github.com/microsoft/vscode/src/vs/editor/common/core/wordHelper.ts
645+ const USUAL_WORD_SEPARATORS = '`~!@#$%^&*()-=+[{]}\\|;:\'",.<>/?' ;
646+
647+ /**
648+ * Create a word definition regular expression based on default word separators.
649+ * Optionally provide allowed separators that should be included in words.
650+ *
651+ * The default would look like this:
652+ * /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g
653+ */
654+ function createWordRegExp ( allowInWords : string = '' ) : RegExp {
655+ let source = '(-?\\d*\\.\\d\\w*)|([^' ;
656+ for ( const sep of USUAL_WORD_SEPARATORS ) {
657+ if ( allowInWords . indexOf ( sep ) >= 0 ) {
658+ continue ;
659+ }
660+ source += '\\' + sep ;
661+ }
662+ source += '\\s]+)' ;
663+ return new RegExp ( source , 'g' ) ;
664+ }
665+
666+ // catches numbers (including floating numbers) in the first group, and alphanum in the second
667+ const DEFAULT_WORD_REGEXP = createWordRegExp ( ) ;
668+
669+ class TextDocumentSnapshot implements TextDocument {
670+
671+ private readonly _extTextDocument : TextDocument ;
672+ private readonly _capturedTextDocument : TextDocumentImpl ;
673+
674+ private readonly _content : string ;
675+ private readonly _uri : VUri ;
676+ private readonly _fileName : string ;
677+ private readonly _languageId : string ;
678+ private readonly _version : number ;
679+ private readonly _eol : EndOfLine ;
680+ private readonly _isUntitled : boolean ;
681+ private readonly _encoding : string ;
682+ private readonly _isDirty : boolean ;
683+ private readonly _isClosed : boolean ;
684+
685+ constructor ( textDocument : TextDocument ) {
686+ this . _extTextDocument = textDocument ;
687+ this . _content = textDocument . getText ( ) ;
688+ this . _uri = textDocument . uri ;
689+ this . _fileName = textDocument . fileName ;
690+ this . _languageId = textDocument . languageId ;
691+ this . _version = textDocument . version ;
692+ this . _eol = textDocument . eol ;
693+ this . _isUntitled = textDocument . isUntitled ;
694+ this . _encoding = textDocument . encoding ;
695+ this . _isDirty = textDocument . isDirty ;
696+ this . _isClosed = textDocument . isClosed ;
697+
698+ this . _capturedTextDocument = TextDocumentImpl . create ( this . _uri . toString ( ) , this . _languageId , this . _version , this . _content ) ;
699+ }
700+
701+ public get uri ( ) : VUri {
702+ return this . _uri ;
703+ }
704+
705+ public get languageId ( ) : string {
706+ return this . _languageId ;
707+ }
708+
709+ public get version ( ) : number {
710+ return this . _version ;
711+ }
712+
713+ public get eol ( ) : EndOfLine {
714+ return this . _eol ;
715+ }
716+
717+ public get isUntitled ( ) : boolean {
718+ return this . _isUntitled ;
719+ }
720+
721+ public get encoding ( ) : string {
722+ return this . _encoding ;
723+ }
724+
725+ public get fileName ( ) : string {
726+ return this . _fileName ;
727+ }
728+
729+ public get isDirty ( ) : boolean {
730+ return this . _isDirty ;
731+ }
732+
733+ public get isClosed ( ) : boolean {
734+ return this . _isClosed ;
735+ }
736+
737+ public save ( ) : Thenable < boolean > {
738+ return this . version === this . _extTextDocument . version
739+ ? this . _extTextDocument . save ( )
740+ : Promise . resolve ( false ) ;
741+ }
742+
743+ public get lineCount ( ) : number {
744+ return this . _capturedTextDocument . lineCount ;
745+ }
746+
747+ public offsetAt ( position : VPosition ) : number {
748+ return this . _capturedTextDocument . offsetAt ( position ) ;
749+ }
750+
751+ public positionAt ( offset : number ) : VPosition {
752+ const position = this . _capturedTextDocument . positionAt ( offset ) ;
753+ return new VPosition ( position . line , position . character ) ;
754+ }
755+
756+ public getText ( range ?: VRange ) : string {
757+ return this . _capturedTextDocument . getText ( range ) ;
758+ }
759+
760+ public lineAt ( line : number ) : TextLine ;
761+ public lineAt ( position : VPosition ) : TextLine ;
762+ public lineAt ( lineOrPosition : VPosition | number ) : TextLine {
763+ const line = typeof lineOrPosition === 'number' ? lineOrPosition : this . validatePosition ( lineOrPosition ) . line ;
764+ if ( line < 0 || line >= this . lineCount ) {
765+ throw new RangeError ( `Illegal value for line: ${ line } ` ) ;
766+ }
767+ const lineRange = this . _capturedTextDocument . getLineRange ( line ) ;
768+ const text = this . _capturedTextDocument . getText ( lineRange ) ;
769+ const firstNonWhitespaceCharacterIndex = text . search ( / \S / ) ;
770+ const range = new VRange ( lineRange . start . line , lineRange . start . character , lineRange . end . line , lineRange . end . character ) ;
771+ const rangeIncludingLineBreak = line + 1 < this . lineCount
772+ ? new VRange ( range . start . line , range . start . character , line + 1 , 0 )
773+ : range ;
774+ return {
775+ lineNumber : line ,
776+ text,
777+ range,
778+ rangeIncludingLineBreak,
779+ firstNonWhitespaceCharacterIndex : firstNonWhitespaceCharacterIndex === - 1 ? text . length : firstNonWhitespaceCharacterIndex ,
780+ isEmptyOrWhitespace : firstNonWhitespaceCharacterIndex === - 1
781+ } ;
782+ }
783+
784+ getWordRangeAtPosition ( position : VPosition , regex ?: RegExp ) : VRange | undefined {
785+ const lineNumber = this . validatePosition ( position ) . line ;
786+ const lineText = this . lineAt ( lineNumber ) . text ;
787+
788+ const wordRegex = TextDocumentSnapshot . getWordRegExp ( regex ) ;
789+
790+ let match ;
791+ wordRegex . lastIndex = 0 ;
792+ while ( ( match = wordRegex . exec ( lineText ) ) !== null ) {
793+ if ( match . index <= position . character && wordRegex . lastIndex >= position . character ) {
794+ return new VRange ( lineNumber , match . index , lineNumber , wordRegex . lastIndex ) ;
795+ }
796+ }
797+
798+ return undefined ;
799+ }
800+
801+ validateRange ( range : VRange ) : VRange {
802+ const start = this . validatePosition ( range . start ) ;
803+ const end = this . validatePosition ( range . end ) ;
804+
805+ if ( start === range . start && end === range . end ) {
806+ return range ;
807+ }
808+ return new VRange ( start . line , start . character , end . line , end . character ) ;
809+ }
810+
811+ validatePosition ( position : VPosition ) : VPosition {
812+ const line = Math . min ( Math . max ( position . line , 0 ) , this . lineCount - 1 ) ;
813+ const lineRange = this . _capturedTextDocument . getLineRange ( line ) ;
814+ const character = Math . min ( Math . max ( position . character , 0 ) , lineRange . end . character ) ;
815+ if ( line === position . line && character === position . character ) {
816+ return position ;
817+ }
818+ return new VPosition ( line , character ) ;
819+ }
820+
821+ private static getWordRegExp ( regex ?: RegExp ) : RegExp {
822+ const result = regex ?? DEFAULT_WORD_REGEXP ;
823+ if ( result . flags . includes ( 'g' ) ) {
824+ return result ;
825+ }
826+ const flags = `${ result . flags } g` ;
827+ return new RegExp ( result . source , flags ) ;
828+ }
635829}
0 commit comments