1
+ import * as fs from 'fs' ;
2
+ import * as ts from 'typescript' ;
3
+
4
+ import { LanguageService } from '@angular/language-service' ;
5
+
6
+ import {
7
+ IConnection , TextDocumentSyncKind , RemoteConsole ,
8
+ TextDocument , TextDocumentIdentifier , Diagnostic , DiagnosticSeverity ,
9
+ InitializeParams , InitializeResult , TextDocumentPositionParams ,
10
+ CompletionItem , CompletionItemKind , Position
11
+ } from 'vscode-languageserver' ;
12
+
13
+ import { Logger as ProjectLogger , ProjectService , ProjectServiceEvent , ProjectServiceHost } from './editorServices' ;
14
+
15
+ // Delegate project service host to TypeScript's sys implementation
16
+ class ProjectServiceHostImpl implements ProjectServiceHost {
17
+ getCurrentDirectory ( ) : string {
18
+ return ts . sys . getCurrentDirectory ( ) ;
19
+ }
20
+
21
+ readFile ( path : string , encoding ?: string ) : string {
22
+ return ts . sys . readFile ( path , encoding ) ;
23
+ }
24
+
25
+ directoryExists ( path : string ) : boolean {
26
+ return ts . sys . directoryExists ( path ) ;
27
+ }
28
+
29
+ getExecutingFilePath ( ) : string {
30
+ return ts . sys . getExecutingFilePath ( ) ;
31
+ }
32
+
33
+ resolvePath ( path : string ) : string {
34
+ return ts . sys . resolvePath ( path ) ;
35
+ }
36
+
37
+ fileExists ( path : string ) : boolean {
38
+ return ts . sys . fileExists ( path ) ;
39
+ }
40
+
41
+ getDirectories ( path : string ) : string [ ] {
42
+ return ts . sys . getDirectories ( path ) ;
43
+ }
44
+
45
+ watchDirectory ( path : string , callback : ts . DirectoryWatcherCallback , recursive ?: boolean ) : ts . FileWatcher {
46
+ return ts . sys . watchDirectory ( path , callback , recursive ) ;
47
+ }
48
+
49
+ watchFile ( path : string , callback : ts . FileWatcherCallback ) : ts . FileWatcher {
50
+ return ts . sys . watchFile ( path , callback ) ;
51
+ }
52
+
53
+ readDirectory ( path : string , extensions ?: string [ ] , exclude ?: string [ ] , include ?: string [ ] ) : string [ ] {
54
+ return ts . sys . readDirectory ( path , extensions , exclude , include ) ;
55
+ }
56
+
57
+ get useCaseSensitiveFileNames ( ) {
58
+ return ts . sys . useCaseSensitiveFileNames ;
59
+ }
60
+
61
+ get newLine ( ) : string {
62
+ return ts . sys . newLine ;
63
+ }
64
+
65
+ setTimeout ( callback : ( ...args : any [ ] ) => void , ms : number , ...args : any [ ] ) : any {
66
+ return setTimeout ( callback , ms , ...args ) ;
67
+ }
68
+
69
+ clearTimeout ( timeoutId : any ) : void {
70
+ return clearTimeout ( timeoutId ) ;
71
+ }
72
+ }
73
+
74
+ class ProjectLoggerImpl implements ProjectLogger {
75
+ private console : RemoteConsole ;
76
+
77
+ connect ( console : RemoteConsole ) {
78
+ this . console = console ;
79
+ }
80
+
81
+ close ( ) : void {
82
+ this . console = null ;
83
+ }
84
+
85
+ isVerbose ( ) : boolean {
86
+ return false ;
87
+ }
88
+
89
+ info ( s : string ) : void {
90
+ if ( this . console )
91
+ this . console . info ( s ) ;
92
+ }
93
+
94
+ startGroup ( ) : void { }
95
+ endGroup ( ) : void { }
96
+
97
+ msg ( s : string , type ?: string ) : void {
98
+ if ( this . console )
99
+ this . console . log ( s ) ;
100
+ }
101
+ }
102
+
103
+ const fileProtocol = "file://" ;
104
+ function uriToFileName ( uri : string ) : string {
105
+ if ( uri && uri . startsWith ( fileProtocol ) ) {
106
+ return uri . substr ( fileProtocol . length ) ;
107
+ }
108
+ return uri ;
109
+ }
110
+ function fileNameToUri ( fileName : string ) : string {
111
+ return fileProtocol + fileName ;
112
+ }
113
+
114
+ export interface NgServiceInfo {
115
+ fileName : string ;
116
+ service ?: LanguageService ;
117
+ offset ?: number ;
118
+ }
119
+
120
+ export interface TextDocumentEvent {
121
+ kind : 'context' | 'opened' | 'closed' | 'change' ;
122
+ document : TextDocumentIdentifier ;
123
+ }
124
+
125
+ export class TextDocuments {
126
+ private projectService : ProjectService ;
127
+ private logger : ProjectLoggerImpl ;
128
+ private host : ProjectServiceHostImpl ;
129
+ private changeNumber = 0 ;
130
+
131
+ constructor ( private event ?: ( event : TextDocumentEvent ) => void ) {
132
+ this . logger = new ProjectLoggerImpl ( ) ;
133
+ this . host = new ProjectServiceHostImpl ( ) ;
134
+ this . projectService = new ProjectService ( this . host , this . logger , this . handleProjectEvent . bind ( this ) ) ;
135
+ }
136
+
137
+ public get syncKind ( ) : TextDocumentSyncKind {
138
+ return TextDocumentSyncKind . Incremental ;
139
+ }
140
+
141
+ public listen ( connection : IConnection ) {
142
+ // Connect the logger to the connection
143
+ this . logger . connect ( connection . console ) ;
144
+
145
+ connection . onDidOpenTextDocument ( event => {
146
+ // An interersting text document was opened in the client. Inform TypeScirpt's project services about it.
147
+ const file = uriToFileName ( event . textDocument . uri ) ;
148
+ const { configFileName, configFileErrors } = this . projectService . openClientFile ( file , event . textDocument . text ) ;
149
+ if ( configFileErrors && configFileErrors . length ) {
150
+ // TODO: Report errors
151
+ this . logger . msg ( `Config errors encountered and need to be reported: ${ configFileErrors . length } \n ${ configFileErrors . map ( error => error . messageText ) . join ( '\n ' ) } ` ) ;
152
+ }
153
+ } ) ;
154
+
155
+ connection . onDidCloseTextDocument ( event => {
156
+ const file = uriToFileName ( event . textDocument . uri ) ;
157
+ this . projectService . closeClientFile ( file ) ;
158
+ } ) ;
159
+
160
+ connection . onDidChangeTextDocument ( event => {
161
+ const file = uriToFileName ( event . textDocument . uri ) ;
162
+ const positions = this . projectService . lineOffsetsToPositions ( file ,
163
+ ( [ ] as { line : number , col : number } [ ] ) . concat ( ...event . contentChanges . map ( change => [ {
164
+ // VSCode is 0 based, editor services is 1 based.
165
+ line : change . range . start . line + 1 ,
166
+ col : change . range . start . character + 1
167
+ } , {
168
+ line : change . range . end . line + 1 ,
169
+ col : change . range . end . character + 1
170
+ } ] ) ) ) ;
171
+ if ( positions ) {
172
+ this . changeNumber ++ ;
173
+ const mappedChanges = event . contentChanges . map ( ( change , i ) => {
174
+ const start = positions [ i % 2 ] ;
175
+ const end = positions [ i % 2 + 1 ] ;
176
+ return { start, end, insertText : change . text } ;
177
+ } ) ;
178
+ this . projectService . clientFileChanges ( file , mappedChanges ) ;
179
+ this . changeNumber ++ ;
180
+ }
181
+ } ) ;
182
+
183
+ connection . onDidSaveTextDocument ( event => {
184
+ // If the file is saved, force the content to be reloaded from disk as the content might have changed on save.
185
+ this . changeNumber ++ ;
186
+ const file = uriToFileName ( event . textDocument . uri ) ;
187
+ const savedContent = this . host . readFile ( file ) ;
188
+ this . projectService . closeClientFile ( file ) ;
189
+ this . projectService . openClientFile ( file , savedContent ) ;
190
+ this . changeNumber ++ ;
191
+ } ) ;
192
+ }
193
+
194
+ public offsetsToPositions ( document : TextDocumentIdentifier , offsets : number [ ] ) : Position [ ] {
195
+ const file = uriToFileName ( document . uri ) ;
196
+ return this . projectService . positionsToLineOffsets ( file , offsets ) . map ( lineOffset => Position . create ( lineOffset . line , lineOffset . col ) ) ;
197
+ }
198
+
199
+ public getNgService ( document : TextDocumentIdentifier ) : LanguageService | undefined {
200
+ return this . getServiceInfo ( document ) . service ;
201
+ }
202
+
203
+ public getServiceInfo ( document : TextDocumentIdentifier , position ?: Position ) : NgServiceInfo {
204
+ const fileName = uriToFileName ( document . uri ) ;
205
+ const project = this . projectService . getProjectForFile ( fileName ) ;
206
+ if ( project ) {
207
+ const service = project . compilerService . ngService ;
208
+ if ( position ) {
209
+ // VSCode is 0 based, editor services are 1 based.
210
+ const offset = this . projectService . lineOffsetsToPositions ( fileName , [ { line : position . line + 1 , col : position . character + 1 } ] ) [ 0 ] ;
211
+ return { fileName, service, offset} ;
212
+ }
213
+ return { fileName, service} ;
214
+ }
215
+ return { fileName} ;
216
+ }
217
+
218
+ public ifUnchanged ( f : ( ) => void ) : ( ) => void {
219
+ const currentChange = this . changeNumber ;
220
+ return ( ) => {
221
+ if ( currentChange == this . changeNumber ) f ( ) ;
222
+ } ;
223
+ }
224
+
225
+ private handleProjectEvent ( event : ProjectServiceEvent ) {
226
+ if ( this . event ) {
227
+ switch ( event . eventName ) {
228
+ case 'context' :
229
+ case 'opened' :
230
+ case 'closed' :
231
+ case 'change' :
232
+ this . event ( { kind : event . eventName , document : { uri : fileNameToUri ( event . data . fileName ) } } ) ;
233
+ }
234
+ }
235
+ }
236
+ }
0 commit comments