-
-
Notifications
You must be signed in to change notification settings - Fork 594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add vue.d.ts and typescript support #94
Changes from 2 commits
9530386
980a918
ed82b9b
fca1190
ad75fa6
164e998
33f3ba7
c99fab7
b02e3fb
a385fd7
a5a3387
144bf54
b496266
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,56 +1,97 @@ | ||
import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache'; | ||
import { SymbolInformation, SymbolKind, 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, startsWith, isWhitespaceOnly, repeat } from '../utils/strings'; | ||
import { getWordAtText, isWhitespaceOnly, repeat } from '../utils/strings'; | ||
import { HTMLDocumentRegions } from './embeddedSupport'; | ||
import path = require('path'); | ||
import url = require('url'); | ||
|
||
import * as ts from 'typescript'; | ||
import { join } from 'path'; | ||
import { createUpdater, parseVue, isVue } from './typescriptMode'; | ||
|
||
const FILE_NAME = 'vscode://javascript/1'; // the same 'file' is used for all contents | ||
const JQUERY_D_TS = join(__dirname, '../../lib/jquery.d.ts'); | ||
import * as ts from 'typescript'; | ||
|
||
const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g; | ||
|
||
export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>): LanguageMode { | ||
let jsDocuments = getLanguageModelCache<TextDocument>(10, 60, document => documentRegions.get(document).getEmbeddedDocument('javascript')); | ||
export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>, workspacePath: string): LanguageMode { | ||
let jsDocuments = getLanguageModelCache<TextDocument>(10, 60, document => { | ||
const vueDocument = documentRegions.get(document); | ||
if (vueDocument.getLanguagesInDocument().indexOf('typescript') > -1) { | ||
return vueDocument.getEmbeddedDocument('typescript'); | ||
} | ||
return vueDocument.getEmbeddedDocument('javascript'); | ||
}); | ||
|
||
let compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es6.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic }; | ||
let currentTextDocument: TextDocument; | ||
let scriptFileVersion: number = 0; | ||
let versions: ts.MapLike<number> = {}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can use ES6 Map There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
let docs: ts.MapLike<TextDocument> = {}; | ||
function updateCurrentTextDocument(doc: TextDocument) { | ||
if (!currentTextDocument || doc.uri !== currentTextDocument.uri || doc.version !== currentTextDocument.version) { | ||
currentTextDocument = jsDocuments.get(doc); | ||
scriptFileVersion++; | ||
const fileName = trimFileUri(currentTextDocument.uri); | ||
if (docs[fileName] && currentTextDocument.languageId !== docs[fileName].languageId) { | ||
// if languageId changed, we must restart the language service; it can't handle file type changes | ||
jsLanguageService = ts.createLanguageService(host); | ||
} | ||
docs[fileName] = currentTextDocument; | ||
versions[fileName] = (versions[fileName] || 0) + 1; | ||
} | ||
} | ||
let host = { | ||
|
||
// Patch typescript functions to insert `import Vue from 'vue'` and `new Vue` around export default. | ||
const { createLanguageServiceSourceFile, updateLanguageServiceSourceFile } = createUpdater(); | ||
(ts as any).createLanguageServiceSourceFile = createLanguageServiceSourceFile; | ||
(ts as any).updateLanguageServiceSourceFile = updateLanguageServiceSourceFile; | ||
const configFile = ts.findConfigFile(workspacePath, ts.sys.fileExists, 'tsconfig.json') || | ||
ts.findConfigFile(workspacePath, ts.sys.fileExists, 'jsconfig.json'); | ||
const files = ts.parseJsonConfigFileContent({}, | ||
ts.sys, | ||
workspacePath, | ||
compilerOptions, | ||
configFile, | ||
undefined, | ||
[{ extension: 'vue', isMixedContent: true }]).fileNames; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should also set compilerOptions to the options from the file. we also need to set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
|
||
let host: ts.LanguageServiceHost = { | ||
getCompilationSettings: () => compilerOptions, | ||
getScriptFileNames: () => [FILE_NAME, JQUERY_D_TS], | ||
getScriptVersion: (fileName: string) => { | ||
if (fileName === FILE_NAME) { | ||
return String(scriptFileVersion); | ||
getScriptFileNames: () => files, | ||
getScriptVersion: filename => filename in versions ? versions[filename].toString() : '1', | ||
getScriptKind(fileName) { | ||
if(isVue(fileName) && docs[fileName]) { | ||
return docs[fileName].languageId === 'typescript' ? ts.ScriptKind.TS : ts.ScriptKind.JS; | ||
} | ||
return '1'; // default lib an jquery.d.ts are static | ||
else { | ||
return (ts as any).getScriptKindFromFileName(fileName); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to expose this on the API for TS 2.3, also add a comment allowing removal of this cast. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added comment. I'll send out a PR for the API change after this. |
||
} | ||
}, | ||
resolveModuleNames(moduleNames: string[], containingFile: string): ts.ResolvedModule[] { | ||
// in the normal case, delegate to ts.resolveModuleName | ||
// in the relative-imported.vue case, manually build a resolved filename | ||
return moduleNames.map(name => | ||
path.isAbsolute(name) || !isVue(name) ? | ||
ts.resolveModuleName(name, containingFile, compilerOptions, ts.sys).resolvedModule : | ||
{ | ||
resolvedFileName: path.join(path.dirname(containingFile), name), | ||
extension: docs[name] && docs[name].languageId === 'typescript' ? ts.Extension.Ts : ts.Extension.Js, | ||
}) | ||
}, | ||
getScriptSnapshot: (fileName: string) => { | ||
let text = ''; | ||
if (startsWith(fileName, 'vscode:')) { | ||
if (fileName === FILE_NAME) { | ||
text = currentTextDocument.getText(); | ||
} | ||
} else { | ||
text = ts.sys.readFile(fileName) || ''; | ||
let text = fileName in docs ? docs[fileName].getText() : (ts.sys.readFile(fileName) || ''); | ||
if (isVue(fileName)) { | ||
// Note: This is required in addition to the parsing in embeddedSupport because | ||
// this works for .vue files that aren't even loaded by VS Code yet. | ||
text = parseVue(text); | ||
} | ||
return { | ||
getText: (start, end) => text.substring(start, end), | ||
getLength: () => text.length, | ||
getChangeRange: () => void 0 | ||
}; | ||
}, | ||
getCurrentDirectory: () => '', | ||
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options) | ||
getCurrentDirectory: () => workspacePath, | ||
getDefaultLibFileName: ts.getDefaultLibFilePath, | ||
}; | ||
|
||
let jsLanguageService = ts.createLanguageService(host); | ||
|
||
let settings: any = {}; | ||
|
@@ -64,8 +105,11 @@ export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocume | |
}, | ||
doValidation(document: TextDocument): Diagnostic[] { | ||
updateCurrentTextDocument(document); | ||
const diagnostics = jsLanguageService.getSyntacticDiagnostics(FILE_NAME); | ||
return diagnostics.map((diag): Diagnostic => { | ||
const filename = trimFileUri(document.uri); | ||
const diagnostics = [...jsLanguageService.getSyntacticDiagnostics(filename), | ||
...jsLanguageService.getSemanticDiagnostics(filename)]; | ||
|
||
return diagnostics.map(diag => { | ||
return { | ||
range: convertRange(currentTextDocument, diag), | ||
severity: DiagnosticSeverity.Error, | ||
|
@@ -75,8 +119,9 @@ export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocume | |
}, | ||
doComplete(document: TextDocument, position: Position): CompletionList { | ||
updateCurrentTextDocument(document); | ||
let filename = trimFileUri(document.uri); | ||
let offset = currentTextDocument.offsetAt(position); | ||
let completions = jsLanguageService.getCompletionsAtPosition(FILE_NAME, offset); | ||
let completions = jsLanguageService.getCompletionsAtPosition(filename, offset); | ||
if (!completions) { | ||
return { isIncomplete: false, items: [] }; | ||
} | ||
|
@@ -102,7 +147,8 @@ export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocume | |
}, | ||
doResolve(document: TextDocument, item: CompletionItem): CompletionItem { | ||
updateCurrentTextDocument(document); | ||
let details = jsLanguageService.getCompletionEntryDetails(FILE_NAME, item.data.offset, item.label); | ||
let filename = trimFileUri(document.uri); | ||
let details = jsLanguageService.getCompletionEntryDetails(filename, item.data.offset, item.label); | ||
if (details) { | ||
item.detail = ts.displayPartsToString(details.displayParts); | ||
item.documentation = ts.displayPartsToString(details.documentation); | ||
|
@@ -112,7 +158,8 @@ export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocume | |
}, | ||
doHover(document: TextDocument, position: Position): Hover { | ||
updateCurrentTextDocument(document); | ||
let info = jsLanguageService.getQuickInfoAtPosition(FILE_NAME, currentTextDocument.offsetAt(position)); | ||
let filename = trimFileUri(document.uri); | ||
let info = jsLanguageService.getQuickInfoAtPosition(filename, currentTextDocument.offsetAt(position)); | ||
if (info) { | ||
let contents = ts.displayPartsToString(info.displayParts); | ||
return { | ||
|
@@ -124,7 +171,8 @@ export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocume | |
}, | ||
doSignatureHelp(document: TextDocument, position: Position): SignatureHelp { | ||
updateCurrentTextDocument(document); | ||
let signHelp = jsLanguageService.getSignatureHelpItems(FILE_NAME, currentTextDocument.offsetAt(position)); | ||
let filename = trimFileUri(document.uri); | ||
let signHelp = jsLanguageService.getSignatureHelpItems(filename, currentTextDocument.offsetAt(position)); | ||
if (signHelp) { | ||
let ret: SignatureHelp = { | ||
activeSignature: signHelp.selectedItemIndex, | ||
|
@@ -161,7 +209,8 @@ export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocume | |
}, | ||
findDocumentHighlight(document: TextDocument, position: Position): DocumentHighlight[] { | ||
updateCurrentTextDocument(document); | ||
let occurrences = jsLanguageService.getOccurrencesAtPosition(FILE_NAME, currentTextDocument.offsetAt(position)); | ||
let filename = trimFileUri(document.uri); | ||
let occurrences = jsLanguageService.getOccurrencesAtPosition(filename, currentTextDocument.offsetAt(position)); | ||
if (occurrences) { | ||
return occurrences.map(entry => { | ||
return { | ||
|
@@ -174,7 +223,8 @@ export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocume | |
}, | ||
findDocumentSymbols(document: TextDocument): SymbolInformation[] { | ||
updateCurrentTextDocument(document); | ||
let items = jsLanguageService.getNavigationBarItems(FILE_NAME); | ||
let filename = trimFileUri(document.uri); | ||
let items = jsLanguageService.getNavigationBarItems(filename); | ||
if (items) { | ||
let result: SymbolInformation[] = []; | ||
let existing = {}; | ||
|
@@ -210,9 +260,10 @@ export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocume | |
}, | ||
findDefinition(document: TextDocument, position: Position): Definition { | ||
updateCurrentTextDocument(document); | ||
let definition = jsLanguageService.getDefinitionAtPosition(FILE_NAME, currentTextDocument.offsetAt(position)); | ||
let filename = trimFileUri(document.uri); | ||
let definition = jsLanguageService.getDefinitionAtPosition(filename, currentTextDocument.offsetAt(position)); | ||
if (definition) { | ||
return definition.filter(d => d.fileName === FILE_NAME).map(d => { | ||
return definition.map(d => { | ||
return { | ||
uri: document.uri, | ||
range: convertRange(currentTextDocument, d.textSpan) | ||
|
@@ -223,9 +274,10 @@ export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocume | |
}, | ||
findReferences(document: TextDocument, position: Position): Location[] { | ||
updateCurrentTextDocument(document); | ||
let references = jsLanguageService.getReferencesAtPosition(FILE_NAME, currentTextDocument.offsetAt(position)); | ||
let filename = trimFileUri(document.uri); | ||
let references = jsLanguageService.getReferencesAtPosition(filename, currentTextDocument.offsetAt(position)); | ||
if (references) { | ||
return references.filter(d => d.fileName === FILE_NAME).map(d => { | ||
return references.map(d => { | ||
return { | ||
uri: document.uri, | ||
range: convertRange(currentTextDocument, d.textSpan) | ||
|
@@ -245,7 +297,8 @@ export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocume | |
end -= range.end.character; | ||
lastLineRange = Range.create(Position.create(range.end.line, 0), range.end); | ||
} | ||
let edits = jsLanguageService.getFormattingEditsForRange(FILE_NAME, start, end, formatSettings); | ||
let filename = trimFileUri(document.uri); | ||
let edits = jsLanguageService.getFormattingEditsForRange(filename, start, end, formatSettings); | ||
if (edits) { | ||
let result = []; | ||
for (let edit of edits) { | ||
|
@@ -276,6 +329,10 @@ export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocume | |
}; | ||
}; | ||
|
||
function trimFileUri(uri: string): string { | ||
return url.parse(uri).path; | ||
} | ||
|
||
function convertRange(document: TextDocument, span: { start: number, length: number }): Range { | ||
let startPosition = document.positionAt(span.start); | ||
let endPosition = document.positionAt(span.start + span.length); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import * as ts from 'typescript'; | ||
import path = require('path'); | ||
import { parseComponent } from "vue-template-compiler"; | ||
|
||
export function isVue(filename: string): boolean { | ||
return path.extname(filename) === '.vue'; | ||
} | ||
|
||
export function parseVue(text: string): string { | ||
const output = parseComponent(text, { pad: 'space' }); | ||
if (output && output.script && output.script.content) { | ||
return output.script.content; | ||
} | ||
else { | ||
return text; | ||
} | ||
} | ||
|
||
export function createUpdater() { | ||
const clssf = ts.createLanguageServiceSourceFile; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these should be added to the LanguageServiceHost API in TS 2.3, I would also add a comment here with that to allow removing this global function hijacking. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added comment where the hijacking happens. This part of the code may remain the same since it's basically the same way that the plugin API allows users to wrap the language service. |
||
const ulssf = ts.updateLanguageServiceSourceFile; | ||
return { | ||
createLanguageServiceSourceFile(fileName: string, scriptSnapshot: ts.IScriptSnapshot, scriptTarget: ts.ScriptTarget, version: string, setNodeParents: boolean, scriptKind?: ts.ScriptKind): ts.SourceFile { | ||
let sourceFile = clssf(fileName, scriptSnapshot, scriptTarget, version, setNodeParents, scriptKind); | ||
if (isVue(fileName)) { | ||
modifyVueSource(sourceFile); | ||
} | ||
return sourceFile; | ||
}, | ||
updateLanguageServiceSourceFile(sourceFile: ts.SourceFile, scriptSnapshot: ts.IScriptSnapshot, version: string, textChangeRange: ts.TextChangeRange, aggressiveChecks?: boolean): ts.SourceFile { | ||
sourceFile = ulssf(sourceFile, scriptSnapshot, version, textChangeRange, aggressiveChecks); | ||
if (isVue(sourceFile.fileName)) { | ||
modifyVueSource(sourceFile); | ||
} | ||
return sourceFile; | ||
} | ||
} | ||
} | ||
|
||
/** Works like Array.prototype.find, returning `undefined` if no element satisfying the predicate is found. */ | ||
function find<T>(array: T[], predicate: (element: T, index: number) => boolean): T | undefined { | ||
for (let i = 0; i < array.length; i++) { | ||
const value = array[i]; | ||
if (predicate(value, i)) { | ||
return value; | ||
} | ||
} | ||
return undefined; | ||
} | ||
|
||
function modifyVueSource(sourceFile: ts.SourceFile): void { | ||
const exportDefaultObject = find(sourceFile.statements, st => st.kind === ts.SyntaxKind.ExportAssignment && | ||
(st as ts.ExportAssignment).expression.kind === ts.SyntaxKind.ObjectLiteralExpression); | ||
if (exportDefaultObject) { | ||
// 1. add `import Vue from './vue' | ||
// (the span of the statement is (0,0) to avoid overlapping existing statements) | ||
const zero = <T extends ts.Node>(n: T) => ts.setTextRange(n, { pos: 0, end: 0 }); | ||
const vueImport = zero(ts.createImportDeclaration(undefined, | ||
undefined, | ||
zero(ts.createImportClause(undefined, | ||
zero(ts.createNamedImports([ | ||
zero(ts.createImportSpecifier( | ||
zero(ts.createIdentifier('Vue')), | ||
zero(ts.createIdentifier('Vue'))))])))), | ||
zero(ts.createLiteral('vue')))); | ||
sourceFile.statements.unshift(vueImport); | ||
|
||
// 2. find the export default and wrap it in `new Vue(...)` if it exists and is an object literal | ||
// (the span of the construct call is the same as the object literal) | ||
const objectLiteral = (exportDefaultObject as ts.ExportAssignment).expression as ts.ObjectLiteralExpression; | ||
const o = <T extends ts.TextRange>(n: T) => ts.setTextRange(n, objectLiteral); | ||
const vue = ts.setTextRange(ts.createIdentifier('Vue'), { pos: objectLiteral.pos, end: objectLiteral.pos + 1 }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind giving |
||
(exportDefaultObject as ts.ExportAssignment).expression = o(ts.createNew(vue, undefined, [objectLiteral])); | ||
o(((exportDefaultObject as ts.ExportAssignment).expression as ts.NewExpression).arguments); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this i would defere to
ts.getDefaultCompilerOptions
and just set:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I take that back..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"esnext" didn't ship as a lib target in 2.2, so I'll use "es2017" for now.
(It was committed Dec 30, but I guess it was part of a PR that got merged later.)