Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion extensions/vscode/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang
selectors.push({ language: 'markdown' });
}

activateAutoInsertion(selectors, client); // TODO: implement auto insert .value
activateAutoInsertion(selectors, client);
activateDocumentDropEdit(selectors, client);
activateWriteVirtualFiles('volar.action.writeVirtualFiles', client);

Expand Down
1 change: 1 addition & 0 deletions packages/language-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@vue/shared": "^3.4.0",
"computeds": "^0.0.1",
"path-browserify": "^1.0.1",
"typescript-vue-plugin": "1.8.27",
"volar-service-css": "0.0.31",
"volar-service-emmet": "0.0.31",
"volar-service-html": "0.0.31",
Expand Down
102 changes: 37 additions & 65 deletions packages/language-service/src/plugins/typescript.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
import { ServiceEnvironment, ServicePlugin, ServicePluginInstance } from '@volar/language-service';
import { VueCompilerOptions, VueGeneratedCode, hyphenateTag, scriptRanges } from '@vue/language-core';
import { capitalize } from '@vue/shared';
import { Provide as TSProvide, create as baseCreate } from 'volar-service-typescript';
import * as ts from 'typescript';
import { create as baseCreate } from 'volar-service-typescript';
import type { Data } from 'volar-service-typescript/lib/features/completions/basic';
import type * as vscode from 'vscode-languageserver-protocol';
import { getNameCasing } from '../ideFeatures/nameCasing';
import { TagNameCasing } from '../types';
import { createAddComponentToOptionEdit } from './vue-extract-file';

// TODO: migrate patchs to ts plugin

const asts = new WeakMap<ts.IScriptSnapshot, ts.SourceFile>();

export function getAst(fileName: string, snapshot: ts.IScriptSnapshot, scriptKind?: ts.ScriptKind) {
let ast = asts.get(snapshot);
if (!ast) {
ast = ts.createSourceFile(fileName, snapshot.getText(0, snapshot.getLength()), ts.ScriptTarget.Latest, undefined, scriptKind);
asts.set(snapshot, ast);
}
return ast;
}

export function create(
ts: typeof import('typescript'),
getVueOptions: (env: ServiceEnvironment) => VueCompilerOptions,
): ServicePlugin {

const base = baseCreate(ts);

return {
...base,
create(context): ServicePluginInstance {
Expand Down Expand Up @@ -72,10 +87,6 @@ export function create(

let newName: string | undefined;

if (itemData?.uri && item.additionalTextEdits) {
patchAdditionalTextEdits(itemData.uri, item.additionalTextEdits);
}

for (const ext of getVueOptions(context.env).extensions) {
const suffix = capitalize(ext.substring('.'.length)); // .vue -> Vue
if (
Expand Down Expand Up @@ -117,22 +128,26 @@ export function create(

const data: Data = item.data;
if (item.data?.__isComponentAutoImport && data && item.additionalTextEdits?.length && item.textEdit && itemData?.uri) {
const langaugeService = context.inject<TSProvide, 'typescript/languageService'>('typescript/languageService');
const [virtualCode] = context.documents.getVirtualCodeByUri(itemData.uri);
const ast = langaugeService.getProgram()?.getSourceFile(itemData.uri);
const exportDefault = ast ? scriptRanges.parseScriptRanges(ts, ast, false, true).exportDefault : undefined;
if (virtualCode && ast && exportDefault) {
const componentName = newName ?? item.textEdit.newText;
const optionEdit = createAddComponentToOptionEdit(ts, ast, componentName);
if (optionEdit) {
const textDoc = context.documents.get(context.documents.getVirtualCodeUri(context.language.files.getByVirtualCode(virtualCode).id, virtualCode.id), virtualCode.languageId, virtualCode.snapshot);
item.additionalTextEdits.push({
range: {
start: textDoc.positionAt(optionEdit.range.start),
end: textDoc.positionAt(optionEdit.range.end),
},
newText: optionEdit.newText,
});
const [virtualCode, sourceFile] = context.documents.getVirtualCodeByUri(itemData.uri);
if (virtualCode && (sourceFile.generated?.code instanceof VueGeneratedCode)) {
const script = sourceFile.generated.languagePlugin.typescript?.getScript(sourceFile.generated.code);
if (script) {
const ast = getAst(sourceFile.generated.code.fileName, script.code.snapshot, script.scriptKind);
const exportDefault = scriptRanges.parseScriptRanges(ts, ast, false, true).exportDefault;
if (exportDefault) {
const componentName = newName ?? item.textEdit.newText;
const optionEdit = createAddComponentToOptionEdit(ts, ast, componentName);
if (optionEdit) {
const textDoc = context.documents.get(context.documents.getVirtualCodeUri(sourceFile.id, virtualCode.id), virtualCode.languageId, virtualCode.snapshot);
item.additionalTextEdits.push({
range: {
start: textDoc.positionAt(optionEdit.range.start),
end: textDoc.positionAt(optionEdit.range.end),
},
newText: optionEdit.newText,
});
}
}
}
}
}
Expand All @@ -143,28 +158,6 @@ export function create(
const result = await baseInstance.provideCodeActions?.(document, range, context, token);
return result?.filter(codeAction => codeAction.title.indexOf('__VLS_') === -1);
},
async resolveCodeAction(item, token) {

const result = await baseInstance.resolveCodeAction?.(item, token) ?? item;

if (result?.edit?.changes) {
for (const uri in result.edit.changes) {
const edits = result.edit.changes[uri];
if (edits) {
patchAdditionalTextEdits(uri, edits);
}
}
}
if (result?.edit?.documentChanges) {
for (const documentChange of result.edit.documentChanges) {
if ('textDocument' in documentChange) {
patchAdditionalTextEdits(documentChange.textDocument.uri, documentChange.edits);
}
}
}

return result;
},
async provideSemanticDiagnostics(document, token) {
const result = await baseInstance.provideSemanticDiagnostics?.(document, token);
return result?.map(diagnostic => {
Expand All @@ -184,24 +177,3 @@ export function create(
},
};
}

// fix https://github.com/vuejs/language-tools/issues/916
function patchAdditionalTextEdits(uri: string, edits: vscode.TextEdit[]) {
if (
uri.endsWith('.vue.js')
|| uri.endsWith('.vue.ts')
|| uri.endsWith('.vue.jsx')
|| uri.endsWith('.vue.tsx')
) {
for (const edit of edits) {
if (
edit.range.start.line === 0
&& edit.range.start.character === 0
&& edit.range.end.line === 0
&& edit.range.end.character === 0
) {
edit.newText = '\n' + edit.newText;
}
}
}
}
60 changes: 20 additions & 40 deletions packages/language-service/src/plugins/vue-autoinsert-dotvalue.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ServicePlugin, ServicePluginInstance } from '@volar/language-service';
import { hyphenateAttr } from '@vue/language-core';
import type * as ts from 'typescript';
import { Provide } from 'volar-service-typescript';
import type * as vscode from 'vscode-languageserver-protocol';
import type { TextDocument } from 'vscode-languageserver-textdocument';
import { getAst } from './typescript';
import { getPropertiesAtLocation } from 'typescript-vue-plugin/out/namedPipe/client';

export function create(ts: typeof import('typescript')): ServicePlugin {
return {
Expand All @@ -22,53 +23,32 @@ export function create(ts: typeof import('typescript')): ServicePlugin {
if (!enabled)
return;

const program = context.inject<Provide, 'typescript/languageService'>('typescript/languageService').getProgram();
if (!program)
return;

const sourceFile = program.getSourceFile(context.env.typescript!.uriToFileName(document.uri));
if (!sourceFile)
return;
const [_, file] = context.documents.getVirtualCodeByUri(document.uri);

if (isBlacklistNode(ts, sourceFile, document.offsetAt(position), false))
return;
let fileName: string | undefined;
let ast: ts.SourceFile | undefined;

const node = findPositionIdentifier(sourceFile, sourceFile, document.offsetAt(position));
if (!node)
return;

const token = context.inject<Provide, 'typescript/languageServiceHost'>('typescript/languageServiceHost').getCancellationToken?.();
if (token) {
context.inject<Provide, 'typescript/languageService'>('typescript/languageService').getQuickInfoAtPosition(context.env.typescript!.uriToFileName(document.uri), node.end);
if (token?.isCancellationRequested()) {
return; // check cancel here because type checker do not use cancel token
if (file?.generated) {
const script = file.generated.languagePlugin.typescript?.getScript(file.generated.code);
if (script) {
fileName = context.env.typescript!.uriToFileName(file.id);
ast = getAst(fileName, script.code.snapshot, script.scriptKind);
}
}

const checker = program.getTypeChecker();
const type = checker.getTypeAtLocation(node);
const props = type.getProperties();

if (props.some(prop => prop.name === 'value')) {
return '${1:.value}';
else if (file) {
fileName = context.env.typescript!.uriToFileName(file.id);
ast = getAst(fileName, file.snapshot);
}

function findPositionIdentifier(sourceFile: ts.SourceFile, node: ts.Node, offset: number) {

let result: ts.Node | undefined;
if (!ast || !fileName)
return;

node.forEachChild(child => {
if (!result) {
if (child.end === offset && ts.isIdentifier(child)) {
result = child;
}
else if (child.end >= offset && child.getStart(sourceFile) < offset) {
result = findPositionIdentifier(sourceFile, child, offset);
}
}
});
if (isBlacklistNode(ts, ast, document.offsetAt(position), false))
return;

return result;
const props = await getPropertiesAtLocation(fileName, document.offsetAt(position)) ?? [];
if (props.some(prop => prop === 'value')) {
return '${1:.value}';
}
},
};
Expand Down
Loading