Skip to content

Commit 4ca1e3d

Browse files
committed
Hooking up @angular/language-service
1 parent 0b0116e commit 4ca1e3d

File tree

6 files changed

+456
-142
lines changed

6 files changed

+456
-142
lines changed

server/package.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,18 @@
1212
"watch": "installServerIntoExtension ../client ./package.json ./tsconfig.json && tsc --watch -p ."
1313
},
1414
"dependencies": {
15-
"typescript": "^2.1.0-dev.20160830",
16-
"vscode-languageserver": "^2.4.0-next.9"
15+
"@angular/language-service": "file:///Users/chuckj/src/angular/dist/packages-dist/language-service",
16+
"@angular/tsc-wrapped": "file:///Users/chuckj/src/angular/dist/tools/@angular/tsc-wrapped",
17+
"reflect-metadata": "^0.1.8",
18+
"rxjs": "^5.0.0-beta.12",
19+
"typescript": "2.0.2",
20+
"vscode-languageserver": "^2.4.0-next.9",
21+
"zone.js": "^0.6.25"
22+
},
23+
"devDependencies": {
24+
"@angular/compiler": "file:///Users/chuckj/src/angular/dist/packages-dist/compiler",
25+
"@angular/compiler-cli": "file:///Users/chuckj/src/angular/dist/packages-dist/compiler-cli",
26+
"@angular/core": "file:///Users/chuckj/src/angular/dist/packages-dist/core",
27+
"@types/node": "^6.0.40"
1728
}
1829
}

server/src/documents.ts

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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

Comments
 (0)