Skip to content

Commit

Permalink
Add coc.preferences.formatterExtension configuration (#5102)
Browse files Browse the repository at this point in the history
  • Loading branch information
statiolake authored Aug 12, 2024
1 parent 38697bc commit bf3d1f6
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 18 deletions.
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

0 comments on commit bf3d1f6

Please sign in to comment.