Skip to content

Commit

Permalink
feat: Support Drag and Drop component into template (#3692)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnsoncodehk authored Oct 31, 2023
1 parent 03a9f11 commit fd38ed0
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 18 deletions.
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

0 comments on commit fd38ed0

Please sign in to comment.