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

Add coc.preferences.formatterExtension configuration #5102

Merged
merged 7 commits into from
Aug 12, 2024
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
6 changes: 6 additions & 0 deletions data/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,12 @@
"type": "string"
}
},
"coc.preferences.formatterExtension": {
"type": ["null", "string"],
"default": null,
"scope": "language-overridable",
"description": "Extension used for formatting documents. When set to null, the formatter with highest priority is used."
},
"coc.preferences.jumpCommand": {
"anyOf": [
{
Expand Down
6 changes: 6 additions & 0 deletions doc/coc-config.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1624,6 +1624,12 @@ Preferences~

Scope: `language-overridable`, default: `false`

"coc.preferences.formatterExtension" *coc-preferences-formatterExtension*

Extension used for formatting documents. When set to null, the formatter with highest priority is used.

Scope: `language-overridable`, default: `null`

"coc.preferences.jumpCommand" *coc-preferences-jumpCommand*

Command used for location jump, like goto definition, goto references
Expand Down
4 changes: 4 additions & 0 deletions history.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 2024-08-12

- Added `coc.preferences.formatterExtension` configuration

# 2024-07-04

- Added `NVIM_APPNAME` support
Expand Down
2 changes: 2 additions & 0 deletions src/language-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ export namespace MessageTransports {
}

export abstract class BaseLanguageClient implements FeatureClient<Middleware, LanguageClientOptions> {
public registeredExtensionName: string

private _id: string
private _name: string
private _clientOptions: ResolvedClientOptions
Expand Down
1 change: 1 addition & 0 deletions src/language-client/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@ import { DidChangeTextDocumentFeatureShape, DidCloseTextDocumentFeatureShape, Di
import { WorkspaceProviderFeature } from './workspaceSymbol'

export interface FeatureClient<M, CO = object> {
registeredExtensionName: string
clientOptions: CO
middleware: M
readonly id: string
Expand Down
11 changes: 8 additions & 3 deletions src/language-client/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider, On
import {
DocumentFormattingRequest, DocumentOnTypeFormattingRequest, DocumentRangeFormattingRequest
} from '../util/protocol'
import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features'
import { FeatureClient, TextDocumentLanguageFeature, ensure } from './features'
import * as cv from './utils/converter'
import * as UUID from './utils/uuid'

Expand Down Expand Up @@ -121,7 +121,8 @@ export class DocumentFormattingFeature extends TextDocumentLanguageFeature<
}

return [
languages.registerDocumentFormatProvider(options.documentSelector!, provider, this._client.clientOptions.formatterPriority),
// We need to pass the originaly registered extension name to keep track of it.
languages.registerDocumentFormatProvider(options.documentSelector!, provider, this._client.clientOptions.formatterPriority, this._client.registeredExtensionName),
provider
]
}
Expand Down Expand Up @@ -176,7 +177,11 @@ export class DocumentRangeFormattingFeature extends TextDocumentLanguageFeature<
}
}

return [languages.registerDocumentRangeFormatProvider(options.documentSelector, provider), provider]
return [
// We need to pass the originaly registered extension name to keep track of it.
languages.registerDocumentRangeFormatProvider(options.documentSelector, provider, undefined, this._client.registeredExtensionName),
provider
]
}
}

Expand Down
27 changes: 20 additions & 7 deletions src/languages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'
import type { LinkedEditingRanges, SignatureHelpContext } from 'vscode-languageserver-protocol'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, CodeAction, CodeActionContext, CodeActionKind, CodeLens, ColorInformation, ColorPresentation, DefinitionLink, DocumentHighlight, DocumentLink, DocumentSymbol, FoldingRange, FormattingOptions, Hover, InlineValue, InlineValueContext, Position, Range, SelectionRange, SemanticTokens, SemanticTokensDelta, SemanticTokensLegend, SignatureHelp, SymbolInformation, TextEdit, TypeHierarchyItem, WorkspaceEdit, WorkspaceSymbol } from 'vscode-languageserver-types'
import { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, CodeAction, CodeActionContext, CodeActionKind, CodeLens, ColorInformation, ColorPresentation, DefinitionLink, DocumentHighlight, DocumentLink, DocumentSymbol, FoldingRange, FormattingOptions, Hover, InlineValue, InlineValueContext, Position, Range, SelectionRange, SemanticTokens, SemanticTokensDelta, SemanticTokensLegend, SignatureHelp, TextEdit, TypeHierarchyItem, WorkspaceEdit, WorkspaceSymbol } from 'vscode-languageserver-types'
import type { Sources } from './completion/sources'
import DiagnosticCollection from './diagnostic/collection'
import diagnosticManager from './diagnostic/manager'
Expand Down Expand Up @@ -35,6 +35,7 @@ import TypeHierarchyManager, { TypeHierarchyItemWithSource } from './provider/ty
import WorkspaceSymbolManager from './provider/workspaceSymbolsManager'
import { LocationWithTarget, TextDocumentMatch } from './types'
import { disposeAll, getConditionValue } from './util'
import { parseExtensionName } from './util/extensionRegistry'
import * as Is from './util/is'
import { CancellationToken, Disposable, Emitter, Event } from './util/protocol'
import { toText } from './util/string'
Expand Down Expand Up @@ -243,12 +244,24 @@ class Languages {
return this.workspaceSymbolsManager.register(provider)
}

public registerDocumentFormatProvider(selector: DocumentSelector, provider: DocumentFormattingEditProvider, priority = 0): Disposable {
return this.formatManager.register(selector, provider, priority)
}

public registerDocumentRangeFormatProvider(selector: DocumentSelector, provider: DocumentRangeFormattingEditProvider, priority = 0): Disposable {
return this.formatRangeManager.register(selector, provider, priority)
// NOTE: The last `extensionName` parameter is not exposed in the index.d.ts since it is only for the internal use
// within coc.nvim. It does not meant to be explicitly specified by extension authors.
public registerDocumentFormatProvider(selector: DocumentSelector, provider: DocumentFormattingEditProvider, priority = 0, extensionName?: string): Disposable {
// To select formatter by extension name, we need to know who registered the formatting provider. This is possible
// by using parseExtensionName() when the extension explicitly called registerDocumentFormatProvider() within its
// activation code. However, when the formatting provider is registered through the language client,
// parseExtensionName() returns just "coc.nvim". But in this case the original extension name extension name should
// be passed as the `extensionName` parameter.
extensionName = extensionName ?? parseExtensionName(Error().stack)
return this.formatManager.register(extensionName, selector, provider, priority)
}

// NOTE: The last `extensionName` parameter is not exposed in the index.d.ts since it is only for the internal use
// within coc.nvim. It does not meant to be explicitly specified by extension authors.
public registerDocumentRangeFormatProvider(selector: DocumentSelector, provider: DocumentRangeFormattingEditProvider, priority = 0, extensionName?: string): Disposable {
// See registerDocumentFormatProvider() for the explanation of the `extensionName` parameter.
extensionName = extensionName ?? parseExtensionName(Error().stack)
return this.formatRangeManager.register(extensionName, selector, provider, priority)
}

public registerCallHierarchyProvider(selector: DocumentSelector, provider: CallHierarchyProvider): Disposable {
Expand Down
37 changes: 33 additions & 4 deletions src/provider/formatManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,28 @@
import { v4 as uuid } from 'uuid'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { FormattingOptions, TextEdit } from 'vscode-languageserver-types'
import { createLogger } from '../logger'
import { TextDocumentMatch } from '../types'
import { CancellationToken, Disposable } from '../util/protocol'
import workspace from '../workspace'
import { DocumentFormattingEditProvider, DocumentSelector } from './index'
import Manager from './manager'
import Manager, { ProviderItem } from './manager'

export default class FormatManager extends Manager<DocumentFormattingEditProvider> {
const logger = createLogger('provider-formatManager')

public register(selector: DocumentSelector, provider: DocumentFormattingEditProvider, priority: number): Disposable {
interface ProviderMeta {
extensionName: string,
}

export default class FormatManager extends Manager<DocumentFormattingEditProvider, ProviderMeta> {

public register(extensionName: string, selector: DocumentSelector, provider: DocumentFormattingEditProvider, priority: number): Disposable {
return this.addProvider({
id: uuid(),
selector,
priority,
provider
provider,
extensionName,
})
}

Expand All @@ -24,7 +34,26 @@ export default class FormatManager extends Manager<DocumentFormattingEditProvide
): Promise<TextEdit[]> {
let item = this.getProvider(document)
if (!item) return null
logger.info("Format by:", item.extensionName)
let { provider } = item
return await Promise.resolve(provider.provideDocumentFormattingEdits(document, options, token))
}

protected override getProvider(document: TextDocumentMatch): ProviderItem<DocumentFormattingEditProvider, ProviderMeta> {
// Prefer user choice
const userChoice = workspace.getConfiguration('coc.preferences', document).get<string>('formatterExtension')
if (userChoice) {
const items = this.getProviders(document)
const userChoiceProvider = items.find(item => item.extensionName === userChoice)
if (userChoiceProvider) {
logger.info("Using user-specified formatter:", userChoice)
return userChoiceProvider
}

logger.error("User-specified formatter not found:", userChoice)
return null
}

return super.getProvider(document)
}
}
37 changes: 33 additions & 4 deletions src/provider/formatRangeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,30 @@
import { v4 as uuid } from 'uuid'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { FormattingOptions, Range, TextEdit } from 'vscode-languageserver-types'
import { createLogger } from '../logger'
import { TextDocumentMatch } from '../types'
import { CancellationToken, Disposable } from '../util/protocol'
import workspace from '../workspace'
import { DocumentRangeFormattingEditProvider, DocumentSelector } from './index'
import Manager from './manager'
import Manager, { ProviderItem } from './manager'

export default class FormatRangeManager extends Manager<DocumentRangeFormattingEditProvider> {
const logger = createLogger('provider-formatRangeManager')

public register(selector: DocumentSelector,
interface ProviderMeta {
extensionName: string,
}

export default class FormatRangeManager extends Manager<DocumentRangeFormattingEditProvider, ProviderMeta> {

public register(extensionName: string, selector: DocumentSelector,
provider: DocumentRangeFormattingEditProvider,
priority: number): Disposable {
return this.addProvider({
id: uuid(),
selector,
provider,
priority
priority,
extensionName,
})
}

Expand All @@ -32,7 +42,26 @@ export default class FormatRangeManager extends Manager<DocumentRangeFormattingE
): Promise<TextEdit[]> {
let item = this.getProvider(document)
if (!item) return null
logger.info("Range format by:", item.extensionName)
let { provider } = item
return await Promise.resolve(provider.provideDocumentRangeFormattingEdits(document, range, options, token))
}

protected override getProvider(document: TextDocumentMatch): ProviderItem<DocumentRangeFormattingEditProvider, ProviderMeta> {
// Prefer user choice
const userChoice = workspace.getConfiguration('coc.preferences', document).get<string>('formatterExtension')
if (userChoice) {
const items = this.getProviders(document)
const userChoiceProvider = items.find(item => item.extensionName === userChoice)
if (userChoiceProvider) {
logger.info("Using user-specified range formatter:", userChoice)
return userChoiceProvider
}

logger.error("User-specified range formatter not found:", userChoice)
return null
}

return super.getProvider(document)
}
}
5 changes: 5 additions & 0 deletions src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { toObject } from './util/object'
import { CancellationToken, Disposable, Emitter, Event } from './util/protocol'
import window from './window'
import workspace from './workspace'
import { parseExtensionName } from './util/extensionRegistry'
const logger = createLogger('services')

export enum ServiceStat {
Expand Down Expand Up @@ -277,12 +278,15 @@ class ServiceManager implements Disposable {
public registerLanguageClient(client: LanguageClient): Disposable
public registerLanguageClient(name: string, config: LanguageServerConfig, folder?: URI): Disposable
public registerLanguageClient(name: string | LanguageClient, config?: LanguageServerConfig, folder?: URI): Disposable {
const registeredExtensionName = parseExtensionName(Error().stack)

let id = typeof name === 'string' ? `languageserver.${name}` : name.id
let disposables: Disposable[] = []
let onDidServiceReady = new Emitter<void>()
let client: LanguageClient | null = typeof name === 'string' ? null : name
if (this.registered.has(id)) return Disposable.create(() => {})
if (client && typeof client.dispose === 'function') disposables.push(client)
if (client) client.registeredExtensionName = registeredExtensionName
let created = false
let service: IServiceProvider = {
id,
Expand All @@ -298,6 +302,7 @@ class ServiceManager implements Disposable {
let opts = getLanguageServerOptions(id, name, config, folder)
if (!opts || config.enable === false) return
client = new LanguageClient(id, name, opts[1], opts[0])
client.registeredExtensionName = registeredExtensionName
service.selector = opts[0].documentSelector
service.client = client
disposables.push(client)
Expand Down