Files
vscode/extensions/html/server/src/modes/javascriptMode.ts
2016-11-23 16:35:50 +01:00

296 lines
11 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { LanguageModelCache } from '../languageModelCache';
import { CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation, Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, MarkedString, DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions } from 'vscode-languageserver-types';
import { LanguageMode } from './languageModes';
import { getWordAtText } from '../utils/words';
import * as ts from 'typescript';
const FILE_NAME = 'vscode://javascript/1'; // the same 'file' is used for all contents
const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
export function getJavascriptMode(jsDocuments: LanguageModelCache<TextDocument>): LanguageMode {
let compilerOptions = { allowNonTsExtensions: true, allowJs: true, target: ts.ScriptTarget.Latest };
let currentTextDocument: TextDocument;
let host = {
getCompilationSettings: () => compilerOptions,
getScriptFileNames: () => [FILE_NAME],
getScriptVersion: (fileName: string) => {
if (fileName === FILE_NAME) {
return String(currentTextDocument.version);
}
return '1'; // default lib is static
},
getScriptSnapshot: (fileName: string) => {
let text = fileName === FILE_NAME ? currentTextDocument.getText() : ts.sys.readFile(fileName);
return {
getText: (start, end) => text.substring(start, end),
getLength: () => text.length,
getChangeRange: () => void 0
};
},
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options)
};
let jsLanguageService = ts.createLanguageService(host);
let settings: any = {};
return {
getId() {
return 'html';
},
configure(options: any) {
settings = options && options.javascript;
},
doValidation(document: TextDocument): Diagnostic[] {
currentTextDocument = jsDocuments.get(document);
const diagnostics = jsLanguageService.getSyntacticDiagnostics(FILE_NAME);
return diagnostics.map(diag => {
return {
range: convertRange(currentTextDocument, diag),
severity: DiagnosticSeverity.Error,
message: ts.flattenDiagnosticMessageText(diag.messageText, '\n')
};
});
},
doComplete(document: TextDocument, position: Position): CompletionList {
currentTextDocument = jsDocuments.get(document);
let offset = currentTextDocument.offsetAt(position);
let completions = jsLanguageService.getCompletionsAtPosition(FILE_NAME, offset);
if (!completions) {
return { isIncomplete: false, items: [] };
}
let replaceRange = convertRange(currentTextDocument, getWordAtText(currentTextDocument.getText(), offset, JS_WORD_REGEX));
return {
isIncomplete: false,
items: completions.entries.map(entry => {
return {
uri: document.uri,
position: position,
label: entry.name,
sortText: entry.sortText,
kind: convertKind(entry.kind),
textEdit: TextEdit.replace(replaceRange, entry.name),
data: { // data used for resolving item details (see 'doResolve')
languageId: 'javascript',
uri: document.uri,
offset: offset
}
};
})
};
},
doResolve(document: TextDocument, item: CompletionItem): CompletionItem {
currentTextDocument = jsDocuments.get(document);
let details = jsLanguageService.getCompletionEntryDetails(FILE_NAME, item.data.offset, item.label);
if (details) {
item.detail = ts.displayPartsToString(details.displayParts);
item.documentation = ts.displayPartsToString(details.documentation);
delete item.data;
}
return item;
},
doHover(document: TextDocument, position: Position): Hover {
currentTextDocument = jsDocuments.get(document);
let info = jsLanguageService.getQuickInfoAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
if (info) {
let contents = ts.displayPartsToString(info.displayParts);
return {
range: convertRange(currentTextDocument, info.textSpan),
contents: MarkedString.fromPlainText(contents)
};
}
return null;
},
doSignatureHelp(document: TextDocument, position: Position): SignatureHelp {
currentTextDocument = jsDocuments.get(document);
let signHelp = jsLanguageService.getSignatureHelpItems(FILE_NAME, currentTextDocument.offsetAt(position));
if (signHelp) {
let ret: SignatureHelp = {
activeSignature: signHelp.selectedItemIndex,
activeParameter: signHelp.argumentIndex,
signatures: []
};
signHelp.items.forEach(item => {
let signature: SignatureInformation = {
label: '',
documentation: null,
parameters: []
};
signature.label += ts.displayPartsToString(item.prefixDisplayParts);
item.parameters.forEach((p, i, a) => {
let label = ts.displayPartsToString(p.displayParts);
let parameter: ParameterInformation = {
label: label,
documentation: ts.displayPartsToString(p.documentation)
};
signature.label += label;
signature.parameters.push(parameter);
if (i < a.length - 1) {
signature.label += ts.displayPartsToString(item.separatorDisplayParts);
}
});
signature.label += ts.displayPartsToString(item.suffixDisplayParts);
ret.signatures.push(signature);
});
return ret;
};
return null;
},
findDocumentHighlight(document: TextDocument, position: Position): DocumentHighlight[] {
currentTextDocument = jsDocuments.get(document);
let occurrences = jsLanguageService.getOccurrencesAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
if (occurrences) {
return occurrences.map(entry => {
return {
range: convertRange(currentTextDocument, entry.textSpan),
kind: entry.isWriteAccess ? DocumentHighlightKind.Write : DocumentHighlightKind.Text
};
});
};
return null;
},
findDefinition(document: TextDocument, position: Position): Definition {
currentTextDocument = jsDocuments.get(document);
let definition = jsLanguageService.getDefinitionAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
if (definition) {
return definition.filter(d => d.fileName === FILE_NAME).map(d => {
return {
uri: document.uri,
range: convertRange(currentTextDocument, d.textSpan)
};
});
}
return null;
},
findReferences(document: TextDocument, position: Position): Location[] {
currentTextDocument = jsDocuments.get(document);
let references = jsLanguageService.getReferencesAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
if (references) {
return references.filter(d => d.fileName === FILE_NAME).map(d => {
return {
uri: document.uri,
range: convertRange(currentTextDocument, d.textSpan)
};
});
}
return null;
},
format(document: TextDocument, range: Range, formatParams: FormattingOptions): TextEdit[] {
currentTextDocument = jsDocuments.get(document);
let initialIndentLevel = computeInitialIndent(document, range, formatParams) + 1;
let formatSettings = convertOptions(formatParams, settings && settings.format, initialIndentLevel);
let start = currentTextDocument.offsetAt(range.start);
let end = currentTextDocument.offsetAt(range.end);
let edits = jsLanguageService.getFormattingEditsForRange(FILE_NAME, start, end, formatSettings);
if (edits) {
let result = [];
for (let edit of edits) {
if (edit.span.start >= start && edit.span.start + edit.span.length <= end) {
result.push({
range: convertRange(currentTextDocument, edit.span),
newText: edit.newText
});
}
}
return result;
}
return null;
},
onDocumentRemoved(document: TextDocument) {
},
dispose() {
jsLanguageService.dispose();
}
};
};
function convertRange(document: TextDocument, span: { start: number, length: number }): Range {
let startPosition = document.positionAt(span.start);
let endPosition = document.positionAt(span.start + span.length);
return Range.create(startPosition, endPosition);
}
function convertKind(kind: string): CompletionItemKind {
switch (kind) {
case 'primitive type':
case 'keyword':
return CompletionItemKind.Keyword;
case 'var':
case 'local var':
return CompletionItemKind.Variable;
case 'property':
case 'getter':
case 'setter':
return CompletionItemKind.Field;
case 'function':
case 'method':
case 'construct':
case 'call':
case 'index':
return CompletionItemKind.Function;
case 'enum':
return CompletionItemKind.Enum;
case 'module':
return CompletionItemKind.Module;
case 'class':
return CompletionItemKind.Class;
case 'interface':
return CompletionItemKind.Interface;
case 'warning':
return CompletionItemKind.File;
}
return CompletionItemKind.Property;
}
function convertOptions(options: FormattingOptions, formatSettings: any, initialIndentLevel: number): ts.FormatCodeOptions {
return {
ConvertTabsToSpaces: options.insertSpaces,
TabSize: options.tabSize,
IndentSize: options.tabSize,
IndentStyle: ts.IndentStyle.Smart,
NewLineCharacter: '\n',
BaseIndentSize: options.tabSize * initialIndentLevel,
InsertSpaceAfterCommaDelimiter: Boolean(!formatSettings || formatSettings.insertSpaceAfterCommaDelimiter),
InsertSpaceAfterSemicolonInForStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterSemicolonInForStatements),
InsertSpaceBeforeAndAfterBinaryOperators: Boolean(!formatSettings || formatSettings.insertSpaceBeforeAndAfterBinaryOperators),
InsertSpaceAfterKeywordsInControlFlowStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterKeywordsInControlFlowStatements),
InsertSpaceAfterFunctionKeywordForAnonymousFunctions: Boolean(!formatSettings || formatSettings.insertSpaceAfterFunctionKeywordForAnonymousFunctions),
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis),
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets),
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces),
PlaceOpenBraceOnNewLineForControlBlocks: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForFunctions),
PlaceOpenBraceOnNewLineForFunctions: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForControlBlocks)
};
}
function computeInitialIndent(document: TextDocument, range: Range, options: FormattingOptions) {
let lineStart = document.offsetAt(Position.create(range.start.line, 0));
let content = document.getText();
let i = lineStart;
let nChars = 0;
let tabSize = options.tabSize || 4;
while (i < content.length) {
let ch = content.charAt(i);
if (ch === ' ') {
nChars++;
} else if (ch === '\t') {
nChars += tabSize;
} else {
break;
}
i++;
}
return Math.floor(nChars / tabSize);
}