From 4c7e6cb5cd3a4b7715e644c2b428c45c2f77f39f Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 28 Oct 2023 04:25:55 +0800 Subject: [PATCH 1/4] feat: drag import --- extensions/vscode/src/common.ts | 2 + extensions/vscode/src/features/dragImport.ts | 44 +++++++++++++++ .../src/languageServerPlugin.ts | 9 +++- packages/language-server/src/protocol.ts | 11 ++++ .../src/ideFeatures/dragImport.ts | 53 +++++++++++++++++++ packages/language-service/src/index.ts | 1 + .../src/plugins/vue-extract-file.ts | 34 ++++++------ 7 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 extensions/vscode/src/features/dragImport.ts create mode 100644 packages/language-service/src/ideFeatures/dragImport.ts diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/common.ts index 0adef5beb4..46960764f6 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/common.ts @@ -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; @@ -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 = { vue: true, diff --git a/extensions/vscode/src/features/dragImport.ts b/extensions/vscode/src/features/dragImport.ts new file mode 100644 index 0000000000..dc0db2f1cf --- /dev/null +++ b/extensions/vscode/src/features/dragImport.ts @@ -0,0 +1,44 @@ +import { GetDragImportEditsRequest } from '@vue/language-server'; +import * as vscode from 'vscode'; +import type { BaseLanguageClient } from 'vscode-languageclient'; + +export async function register(_context: vscode.ExtensionContext, client: BaseLanguageClient) { + vscode.languages.registerDocumentDropEditProvider( + { language: 'vue' }, + { + 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')) { + let tagName = uri.substring(uri.lastIndexOf('/') + 1); + tagName = tagName.substring(0, tagName.lastIndexOf('.')); + const edits = await client.sendRequest(GetDragImportEditsRequest.type, { + uri: document.uri.toString(), + importUri: uri, + tagName, + }); + const additionalEdit = new vscode.WorkspaceEdit(); + for (const edit of edits ?? []) { + 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: new vscode.SnippetString(`<${tagName}$0 />`), + additionalEdit, + }; + } + } + } + }, + } + ); +} diff --git a/packages/language-server/src/languageServerPlugin.ts b/packages/language-server/src/languageServerPlugin.ts index 90b706c6ca..c472834590 100644 --- a/packages/language-server/src/languageServerPlugin.ts +++ b/packages/language-server/src/languageServerPlugin.ts @@ -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, GetDragImportEditsRequest } from './protocol'; import { VueServerInitializationOptions } from './types'; import type * as ts from 'typescript/lib/tsserverlibrary'; import * as componentMeta from 'vue-component-meta'; @@ -100,6 +100,13 @@ export function createServerPlugin(connection: Connection) { } }); + connection.onRequest(GetDragImportEditsRequest.type, async params => { + const languageService = await getService(params.uri); + if (languageService) { + return nameCasing.getDragImportEdits(ts, languageService.context, params.uri, params.importUri, params.tagName); + } + }); + connection.onRequest(GetConvertAttrCasingEditsRequest.type, async params => { const languageService = await getService(params.textDocument.uri); if (languageService) { diff --git a/packages/language-server/src/protocol.ts b/packages/language-server/src/protocol.ts index a36d7c0752..65716f76a5 100644 --- a/packages/language-server/src/protocol.ts +++ b/packages/language-server/src/protocol.ts @@ -31,6 +31,17 @@ export namespace GetConvertTagCasingEditsRequest { export const type = new vscode.RequestType('vue/convertTagNameCasing'); } +export namespace GetDragImportEditsRequest { + export type ParamsType = { + uri: string, + importUri: string, + tagName: string, + }; + export type ResponseType = vscode.TextEdit[] | null | undefined; + export type ErrorType = never; + export const type = new vscode.RequestType('vue/dragImportEdits'); +} + export namespace GetConvertAttrCasingEditsRequest { export type ParamsType = { textDocument: vscode.TextDocumentIdentifier, diff --git a/packages/language-service/src/ideFeatures/dragImport.ts b/packages/language-service/src/ideFeatures/dragImport.ts new file mode 100644 index 0000000000..9e30cb2523 --- /dev/null +++ b/packages/language-service/src/ideFeatures/dragImport.ts @@ -0,0 +1,53 @@ +import { ServiceContext } from '@volar/language-service'; +import { VueFile } from '@vue/language-core'; +import { camelize, capitalize } 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'; + +export function getDragImportEdits( + ts: typeof import('typescript/lib/tsserverlibrary'), + ctx: ServiceContext, + uri: string, + importUri: string, + tagName: string +) { + + const newName = capitalize(camelize(tagName)); + 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 edits; +} diff --git a/packages/language-service/src/index.ts b/packages/language-service/src/index.ts index 75191f5bb7..4626d9053b 100644 --- a/packages/language-service/src/index.ts +++ b/packages/language-service/src/index.ts @@ -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'; diff --git a/packages/language-service/src/plugins/vue-extract-file.ts b/packages/language-service/src/plugins/vue-extract-file.ts index 5c359ede3b..615640b970 100644 --- a/packages/language-service/src/plugins/vue-extract-file.ts +++ b/packages/language-service/src/plugins/vue-extract-file.ts @@ -82,7 +82,7 @@ export const create = function (): Service { const toExtract = collectExtractProps(); const initialIndentSetting = await ctx!.env.getConfiguration!('volar.format.initialIndent') as Record; 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 = []; @@ -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 Date: Tue, 31 Oct 2023 12:14:59 +0800 Subject: [PATCH 2/4] GetDragImportEditsRequest -> GetDragAndDragImportEditsRequest --- extensions/vscode/src/features/dragImport.ts | 4 ++-- packages/language-server/src/languageServerPlugin.ts | 4 ++-- packages/language-server/src/protocol.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/vscode/src/features/dragImport.ts b/extensions/vscode/src/features/dragImport.ts index dc0db2f1cf..41574afc61 100644 --- a/extensions/vscode/src/features/dragImport.ts +++ b/extensions/vscode/src/features/dragImport.ts @@ -1,4 +1,4 @@ -import { GetDragImportEditsRequest } from '@vue/language-server'; +import { GetDragAndDragImportEditsRequest } from '@vue/language-server'; import * as vscode from 'vscode'; import type { BaseLanguageClient } from 'vscode-languageclient'; @@ -13,7 +13,7 @@ export async function register(_context: vscode.ExtensionContext, client: BaseLa if (uri.endsWith('.vue')) { let tagName = uri.substring(uri.lastIndexOf('/') + 1); tagName = tagName.substring(0, tagName.lastIndexOf('.')); - const edits = await client.sendRequest(GetDragImportEditsRequest.type, { + const edits = await client.sendRequest(GetDragAndDragImportEditsRequest.type, { uri: document.uri.toString(), importUri: uri, tagName, diff --git a/packages/language-server/src/languageServerPlugin.ts b/packages/language-server/src/languageServerPlugin.ts index 9760fab126..54db0cc9d5 100644 --- a/packages/language-server/src/languageServerPlugin.ts +++ b/packages/language-server/src/languageServerPlugin.ts @@ -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, GetDragImportEditsRequest } 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'; @@ -100,7 +100,7 @@ export function createServerPlugin(connection: Connection) { } }); - connection.onRequest(GetDragImportEditsRequest.type, async params => { + 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.tagName); diff --git a/packages/language-server/src/protocol.ts b/packages/language-server/src/protocol.ts index 65716f76a5..3a1909c9fa 100644 --- a/packages/language-server/src/protocol.ts +++ b/packages/language-server/src/protocol.ts @@ -31,7 +31,7 @@ export namespace GetConvertTagCasingEditsRequest { export const type = new vscode.RequestType('vue/convertTagNameCasing'); } -export namespace GetDragImportEditsRequest { +export namespace GetDragAndDragImportEditsRequest { export type ParamsType = { uri: string, importUri: string, From 650d0651728b0b2eb0d8f442817e2f358b369ea9 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 31 Oct 2023 12:31:59 +0800 Subject: [PATCH 3/4] respect casing --- extensions/vscode/src/features/dragImport.ts | 20 +++++++++------- .../src/languageServerPlugin.ts | 2 +- packages/language-server/src/protocol.ts | 8 +++++-- .../src/ideFeatures/dragImport.ts | 24 ++++++++++++++----- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/extensions/vscode/src/features/dragImport.ts b/extensions/vscode/src/features/dragImport.ts index 41574afc61..978df5b653 100644 --- a/extensions/vscode/src/features/dragImport.ts +++ b/extensions/vscode/src/features/dragImport.ts @@ -1,6 +1,7 @@ -import { GetDragAndDragImportEditsRequest } from '@vue/language-server'; +import { GetDragAndDragImportEditsRequest, TagNameCasing } from '@vue/language-server'; import * as vscode from 'vscode'; -import type { BaseLanguageClient } from 'vscode-languageclient'; +import type { BaseLanguageClient, InsertTextFormat } from 'vscode-languageclient'; +import { tagNameCasings } from './nameCasing'; export async function register(_context: vscode.ExtensionContext, client: BaseLanguageClient) { vscode.languages.registerDocumentDropEditProvider( @@ -11,15 +12,16 @@ export async function register(_context: vscode.ExtensionContext, client: BaseLa if (mimeType === 'text/uri-list') { const uri = item.value as string; if (uri.endsWith('.vue')) { - let tagName = uri.substring(uri.lastIndexOf('/') + 1); - tagName = tagName.substring(0, tagName.lastIndexOf('.')); - const edits = await client.sendRequest(GetDragAndDragImportEditsRequest.type, { + const response = await client.sendRequest(GetDragAndDragImportEditsRequest.type, { uri: document.uri.toString(), importUri: uri, - tagName, + casing: tagNameCasings.get(document.uri.toString()) ?? TagNameCasing.Pascal, }); + if (!response) { + return; + } const additionalEdit = new vscode.WorkspaceEdit(); - for (const edit of edits ?? []) { + for (const edit of response.additionalEdits) { additionalEdit.replace( document.uri, new vscode.Range( @@ -32,7 +34,9 @@ export async function register(_context: vscode.ExtensionContext, client: BaseLa ); } return { - insertText: new vscode.SnippetString(`<${tagName}$0 />`), + insertText: response.insertTextFormat === 2 satisfies typeof InsertTextFormat.Snippet + ? new vscode.SnippetString(response.insertText) + : response.insertText, additionalEdit, }; } diff --git a/packages/language-server/src/languageServerPlugin.ts b/packages/language-server/src/languageServerPlugin.ts index 54db0cc9d5..7ae55990d5 100644 --- a/packages/language-server/src/languageServerPlugin.ts +++ b/packages/language-server/src/languageServerPlugin.ts @@ -103,7 +103,7 @@ 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.tagName); + return nameCasing.getDragImportEdits(ts, languageService.context, params.uri, params.importUri, params.casing); } }); diff --git a/packages/language-server/src/protocol.ts b/packages/language-server/src/protocol.ts index 3a1909c9fa..e7638c67f8 100644 --- a/packages/language-server/src/protocol.ts +++ b/packages/language-server/src/protocol.ts @@ -35,9 +35,13 @@ export namespace GetDragAndDragImportEditsRequest { export type ParamsType = { uri: string, importUri: string, - tagName: string, + casing: TagNameCasing, }; - export type ResponseType = vscode.TextEdit[] | null | undefined; + export type ResponseType = { + insertText: string; + insertTextFormat: vscode.InsertTextFormat; + additionalEdits: vscode.TextEdit[]; + } | null | undefined; export type ErrorType = never; export const type = new vscode.RequestType('vue/dragImportEdits'); } diff --git a/packages/language-service/src/ideFeatures/dragImport.ts b/packages/language-service/src/ideFeatures/dragImport.ts index 9e30cb2523..e4d2c3d192 100644 --- a/packages/language-service/src/ideFeatures/dragImport.ts +++ b/packages/language-service/src/ideFeatures/dragImport.ts @@ -1,26 +1,34 @@ import { ServiceContext } from '@volar/language-service'; import { VueFile } from '@vue/language-core'; -import { camelize, capitalize } from '@vue/shared'; +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, - tagName: string -) { + casing: TagNameCasing +): { + insertText: string; + insertTextFormat: vscode.InsertTextFormat; + additionalEdits: vscode.TextEdit[]; +} | undefined { - const newName = capitalize(camelize(tagName)); + 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 []; + return; const lastImportNode = getLastImportNode(ts, script.ast); const edits: vscode.TextEdit[] = [ @@ -49,5 +57,9 @@ export function getDragImportEdits( } } - return edits; + return { + insertText: `<${casing === TagNameCasing.Kebab ? hyphenate(newName) : newName}$0 />`, + insertTextFormat: 2 satisfies typeof vscode.InsertTextFormat.Snippet, + additionalEdits: edits, + }; } From 72403396272612595c0864648e4ef521f34c8b2d Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 31 Oct 2023 12:39:08 +0800 Subject: [PATCH 4/4] support for vitepress, petite-vue --- extensions/vscode/src/features/dragImport.ts | 94 ++++++++++++-------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/extensions/vscode/src/features/dragImport.ts b/extensions/vscode/src/features/dragImport.ts index 978df5b653..46878bd4ed 100644 --- a/extensions/vscode/src/features/dragImport.ts +++ b/extensions/vscode/src/features/dragImport.ts @@ -1,48 +1,64 @@ import { GetDragAndDragImportEditsRequest, TagNameCasing } from '@vue/language-server'; import * as vscode from 'vscode'; -import type { BaseLanguageClient, InsertTextFormat } from 'vscode-languageclient'; +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) { - vscode.languages.registerDocumentDropEditProvider( - { language: 'vue' }, - { - 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')) { - 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 - ); +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, + }; } - return { - insertText: response.insertTextFormat === 2 satisfies typeof InsertTextFormat.Snippet - ? new vscode.SnippetString(response.insertText) - : response.insertText, - additionalEdit, - }; } } - } - }, - } + }, + } + ), ); }