Skip to content
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

feat(vscode): supported Component Drag and Drop Import #3692

Merged
merged 5 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 0 deletions extensions/vscode/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { config } from './config';
import * as componentMeta from './features/componentMeta';
import * as doctor from './features/doctor';
import * as nameCasing from './features/nameCasing';
import * as dragImport from './features/dragImport';
import * as splitEditors from './features/splitEditors';

let semanticClient: lsp.BaseLanguageClient;
Expand Down Expand Up @@ -93,6 +94,7 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang
splitEditors.register(context, syntacticClient);
doctor.register(context, semanticClient);
componentMeta.register(context, semanticClient);
dragImport.register(context, semanticClient);

const supportedLanguages: Record<string, boolean> = {
vue: true,
Expand Down
64 changes: 64 additions & 0 deletions extensions/vscode/src/features/dragImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { GetDragAndDragImportEditsRequest, TagNameCasing } from '@vue/language-server';
import * as vscode from 'vscode';
import type { BaseLanguageClient, DocumentFilter, InsertTextFormat } from 'vscode-languageclient';
import { tagNameCasings } from './nameCasing';
import { config } from '../config';

export async function register(context: vscode.ExtensionContext, client: BaseLanguageClient) {

const selectors: DocumentFilter[] = [{ language: 'vue' }];

if (config.server.petiteVue.supportHtmlFile) {
selectors.push({ language: 'html' });
}
if (config.server.vitePress.supportMdFile) {
selectors.push({ language: 'markdown' });
}

context.subscriptions.push(
vscode.languages.registerDocumentDropEditProvider(
selectors,
{
async provideDocumentDropEdits(document, _position, dataTransfer) {
for (const [mimeType, item] of dataTransfer) {
if (mimeType === 'text/uri-list') {
const uri = item.value as string;
if (
uri.endsWith('.vue')
|| (uri.endsWith('.md') && config.server.vitePress.supportMdFile)
) {
const response = await client.sendRequest(GetDragAndDragImportEditsRequest.type, {
uri: document.uri.toString(),
importUri: uri,
casing: tagNameCasings.get(document.uri.toString()) ?? TagNameCasing.Pascal,
});
if (!response) {
return;
}
const additionalEdit = new vscode.WorkspaceEdit();
for (const edit of response.additionalEdits) {
additionalEdit.replace(
document.uri,
new vscode.Range(
edit.range.start.line,
edit.range.start.character,
edit.range.end.line,
edit.range.end.character,
),
edit.newText
);
}
return {
insertText: response.insertTextFormat === 2 satisfies typeof InsertTextFormat.Snippet
? new vscode.SnippetString(response.insertText)
: response.insertText,
additionalEdit,
};
}
}
}
},
}
),
);
}
9 changes: 8 additions & 1 deletion packages/language-server/src/languageServerPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { LanguageServerPlugin, Connection } from '@volar/language-server';
import * as vue from '@vue/language-service';
import * as vue2 from '@vue/language-core';
import * as nameCasing from '@vue/language-service';
import { DetectNameCasingRequest, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest, GetComponentMeta } from './protocol';
import { DetectNameCasingRequest, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest, GetComponentMeta, GetDragAndDragImportEditsRequest } from './protocol';
import { VueServerInitializationOptions } from './types';
import type * as ts from 'typescript/lib/tsserverlibrary';
import * as componentMeta from 'vue-component-meta/out/base';
Expand Down Expand Up @@ -100,6 +100,13 @@ export function createServerPlugin(connection: Connection) {
}
});

connection.onRequest(GetDragAndDragImportEditsRequest.type, async params => {
const languageService = await getService(params.uri);
if (languageService) {
return nameCasing.getDragImportEdits(ts, languageService.context, params.uri, params.importUri, params.casing);
}
});

connection.onRequest(GetConvertAttrCasingEditsRequest.type, async params => {
const languageService = await getService(params.textDocument.uri);
if (languageService) {
Expand Down
15 changes: 15 additions & 0 deletions packages/language-server/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ export namespace GetConvertTagCasingEditsRequest {
export const type = new vscode.RequestType<ParamsType, ResponseType, ErrorType>('vue/convertTagNameCasing');
}

export namespace GetDragAndDragImportEditsRequest {
export type ParamsType = {
uri: string,
importUri: string,
casing: TagNameCasing,
};
export type ResponseType = {
insertText: string;
insertTextFormat: vscode.InsertTextFormat;
additionalEdits: vscode.TextEdit[];
} | null | undefined;
export type ErrorType = never;
export const type = new vscode.RequestType<ParamsType, ResponseType, ErrorType>('vue/dragImportEdits');
}

export namespace GetConvertAttrCasingEditsRequest {
export type ParamsType = {
textDocument: vscode.TextDocumentIdentifier,
Expand Down
65 changes: 65 additions & 0 deletions packages/language-service/src/ideFeatures/dragImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ServiceContext } from '@volar/language-service';
import { VueFile } from '@vue/language-core';
import { camelize, capitalize, hyphenate } from '@vue/shared';
import { posix as path } from 'path';
import type * as vscode from 'vscode-languageserver-protocol';
import { createAddComponentToOptionEdit, getLastImportNode } from '../plugins/vue-extract-file';
import { TagNameCasing } from '../types';

export function getDragImportEdits(
ts: typeof import('typescript/lib/tsserverlibrary'),
ctx: ServiceContext,
uri: string,
importUri: string,
casing: TagNameCasing
): {
insertText: string;
insertTextFormat: vscode.InsertTextFormat;
additionalEdits: vscode.TextEdit[];
} | undefined {

let baseName = importUri.substring(importUri.lastIndexOf('/') + 1);
baseName = baseName.substring(0, baseName.lastIndexOf('.'));

const newName = capitalize(camelize(baseName));
const document = ctx!.getTextDocument(uri)!;
const [vueFile] = ctx!.documents.getVirtualFileByUri(document.uri) as [VueFile, any];
const { sfc } = vueFile;
const script = sfc.scriptSetup ?? sfc.script;

if (!sfc.template || !script)
return;

const lastImportNode = getLastImportNode(ts, script.ast);
const edits: vscode.TextEdit[] = [
{
range: lastImportNode ? {
start: document.positionAt(script.startTagEnd + lastImportNode.end),
end: document.positionAt(script.startTagEnd + lastImportNode.end),
} : {
start: document.positionAt(script.startTagEnd),
end: document.positionAt(script.startTagEnd),
},
newText: `\nimport ${newName} from './${path.relative(path.dirname(uri), importUri) || importUri.substring(importUri.lastIndexOf('/') + 1)}'`,
},
];

if (sfc.script) {
const edit = createAddComponentToOptionEdit(ts, sfc.script.ast, newName);
if (edit) {
edits.push({
range: {
start: document.positionAt(sfc.script.startTagEnd + edit.range.start),
end: document.positionAt(sfc.script.startTagEnd + edit.range.end),
},
newText: edit.newText,
});
}
}

return {
insertText: `<${casing === TagNameCasing.Kebab ? hyphenate(newName) : newName}$0 />`,
insertTextFormat: 2 satisfies typeof vscode.InsertTextFormat.Snippet,
additionalEdits: edits,
};
}
1 change: 1 addition & 0 deletions packages/language-service/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from '@volar/language-service';
export * from '@vue/language-core';
export * from './ideFeatures/nameCasing';
export * from './ideFeatures/dragImport';
export * from './languageService';
export { TagNameCasing, AttrNameCasing } from './types';
export { Provide } from './plugins/vue';
34 changes: 17 additions & 17 deletions packages/language-service/src/plugins/vue-extract-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const create = function (): Service {
const toExtract = collectExtractProps();
const initialIndentSetting = await ctx!.env.getConfiguration!('volar.format.initialIndent') as Record<string, boolean>;
const newUri = document.uri.substring(0, document.uri.lastIndexOf('/') + 1) + `${newName}.vue`;
const lastImportNode = getLastImportNode(script.ast);
const lastImportNode = getLastImportNode(ts, script.ast);

let newFileTags = [];

Expand Down Expand Up @@ -169,22 +169,6 @@ export const create = function (): Service {
},
};

function getLastImportNode(sourceFile: ts.SourceFile) {

let lastImportNode: ts.Node | undefined;

for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement)) {
lastImportNode = statement;
}
else {
break;
}
}

return lastImportNode;
}

function collectExtractProps() {

const result = new Map<string, {
Expand Down Expand Up @@ -315,6 +299,22 @@ function isInitialIndentNeeded(ts: typeof import("typescript/lib/tsserverlibrary
return initialIndentSetting[languageKindIdMap[languageKind]] ?? false;
}

export function getLastImportNode(ts: typeof import('typescript/lib/tsserverlibrary'), sourceFile: ts.SourceFile) {

let lastImportNode: ts.Node | undefined;

for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement)) {
lastImportNode = statement;
}
else {
break;
}
}

return lastImportNode;
}

export function createAddComponentToOptionEdit(ts: typeof import('typescript/lib/tsserverlibrary'), ast: ts.SourceFile, componentName: string) {

const exportDefault = scriptRanges.parseScriptRanges(ts, ast, false, true).exportDefault;
Expand Down
Loading