Skip to content
51 changes: 50 additions & 1 deletion lib/adapters/rename-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,27 @@ import Convert from "../convert"
import { Point, TextEditor } from "atom"
import {
LanguageClientConnection,
PrepareRenameParams,
RenameParams,
ServerCapabilities,
TextDocumentEdit,
ApplyWorkspaceEditResponse,
TextEdit,
Range,
} from "../languageclient"
import ApplyEditAdapter from "./apply-edit-adapter"

export default class RenameAdapter {
public static canAdapt(serverCapabilities: ServerCapabilities): boolean {
return serverCapabilities.renameProvider === true
return serverCapabilities.renameProvider !== false
}

public static canPrepare(serverCapabilities: ServerCapabilities): boolean {
if (serverCapabilities.renameProvider === undefined || typeof serverCapabilities.renameProvider === "boolean") {
return false
}

return serverCapabilities.renameProvider.prepareProvider || false
}

public static async getRename(
Expand All @@ -34,6 +46,36 @@ export default class RenameAdapter {
}
}

public static async rename(
connection: LanguageClientConnection,
editor: TextEditor,
point: Point,
newName: string
): Promise<ApplyWorkspaceEditResponse> {
const edit = await connection.rename(RenameAdapter.createRenameParams(editor, point, newName))
return ApplyEditAdapter.onApplyEdit({ edit })
}

public static async prepareRename(
connection: LanguageClientConnection,
editor: TextEditor,
point: Point
): Promise<{ possible: boolean; range?: Range; label?: string | null }> {
const result = await connection.prepareRename(RenameAdapter.createPrepareRenameParams(editor, point))

if (!result) {
return { possible: false }
}
if ("defaultBehavior" in result) {
return { possible: result.defaultBehavior }
}
return {
possible: true,
range: "range" in result ? result.range : result,
label: "range" in result ? result.placeholder : null,
}
}

public static createRenameParams(editor: TextEditor, point: Point, newName: string): RenameParams {
return {
textDocument: Convert.editorToTextDocumentIdentifier(editor),
Expand All @@ -42,6 +84,13 @@ export default class RenameAdapter {
}
}

public static createPrepareRenameParams(editor: TextEditor, point: Point): PrepareRenameParams {
return {
textDocument: Convert.editorToTextDocumentIdentifier(editor),
position: Convert.pointToPosition(point),
}
}

public static convertChanges(changes: { [uri: string]: TextEdit[] }): Map<atomIde.IdeUri, atomIde.TextEdit[]> {
const result = new Map()
Object.keys(changes).forEach((uri) => {
Expand Down
103 changes: 102 additions & 1 deletion lib/auto-languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ import {
normalizePath,
considerAdditionalPath,
} from "./server-manager.js"
import { Disposable, CompositeDisposable, Point, Range, TextEditor } from "atom"
import { Disposable, CompositeDisposable, Point, Range, TextEditor, CommandEvent, TextEditorElement } from "atom"
import * as ac from "atom/autocomplete-plus"
import Dialog from "./views/dialog"
import { basename } from "path"

export { ActiveServer, LanguageClientConnection, LanguageServerProcess }
Expand Down Expand Up @@ -234,6 +235,7 @@ export default class AutoLanguageClient {
dynamicRegistration: false,
},
rename: {
prepareSupport: true,
dynamicRegistration: false,
},
moniker: {
Expand Down Expand Up @@ -350,6 +352,7 @@ export default class AutoLanguageClient {
this.shutdownGracefully
)
this._serverManager.startListening()
this.registerRenameCommands()
process.on("exit", () => this.exitCleanup.bind(this))
}

Expand Down Expand Up @@ -1033,6 +1036,104 @@ export default class AutoLanguageClient {
}
}

public async registerRenameCommands() {
this._disposable.add(
atom.commands.add("atom-text-editor", "IDE:Rename", async (event: CommandEvent<TextEditorElement>) => {
const textEditorElement = event.currentTarget
const textEditor = textEditorElement.getModel()
const bufferPosition = textEditor.getCursorBufferPosition()
const server = await this._serverManager.getServer(textEditor)

if (!server) {
return
}

if (!RenameAdapter.canAdapt(server.capabilities)) {
atom.notifications.addInfo(`Rename is not supported by ${this.getServerName()}`)
}

const outcome = { possible: true, label: "Rename" }
if (RenameAdapter.canPrepare(server.capabilities)) {
const { possible } = await RenameAdapter.prepareRename(server.connection, textEditor, bufferPosition)
outcome.possible = possible
}

if (!outcome.possible) {
atom.notifications.addWarning(
`Nothing to rename at position at row ${bufferPosition.row + 1} and column ${bufferPosition.column + 1}`
)
return
}
const newName = await Dialog.prompt("Enter new name")
RenameAdapter.rename(server.connection, textEditor, bufferPosition, newName)
return
})
)

this._disposable.add(
atom.contextMenu.add({
"atom-text-editor": [
{
label: "Refactor",
submenu: [{ label: "Rename", command: "IDE:Rename" }],
created: function (event: MouseEvent) {
const textEditor = atom.workspace.getActiveTextEditor()
if (!textEditor) {
return
}

const screenPosition = atom.views.getView(textEditor).getComponent().screenPositionForMouseEvent(event)
const bufferPosition = textEditor.bufferPositionForScreenPosition(screenPosition)

textEditor.setCursorBufferPosition(bufferPosition)
},
},
],
})
)
}

public provideIntentions() {
return {
grammarScopes: this.getGrammarScopes(), // [*] would also work
getIntentions: async ({ textEditor, bufferPosition }: { textEditor: TextEditor; bufferPosition: Point }) => {
const intentions: { title: string; selected: () => void }[] = []
const server = await this._serverManager.getServer(textEditor)

if (server == null) {
return intentions
}

if (RenameAdapter.canAdapt(server.capabilities)) {
const outcome = { possible: true, label: "Rename" }
if (RenameAdapter.canPrepare(server.capabilities)) {
const { possible } = await RenameAdapter.prepareRename(server.connection, textEditor, bufferPosition)
outcome.possible = possible
}

if (outcome.possible) {
intentions.push({
title: outcome.label,
selected: async () => {
const newName = await Dialog.prompt("Enter new name")
return RenameAdapter.rename(server.connection, textEditor, bufferPosition, newName)
},
})
}
}

intentions.push({
title: "Some dummy intention",
selected: async () => {
console.log("selected")
},
})

return intentions
},
}
}

protected async getRename(
editor: TextEditor,
position: Point,
Expand Down
18 changes: 18 additions & 0 deletions lib/languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,24 @@ export class LanguageClientConnection extends EventEmitter {
return this._sendRequest(lsp.DocumentOnTypeFormattingRequest.type, params)
}

/**
* Public: Send a `textDocument/prepareRename` request.
*
* @param params The {PrepareRenameParams} identifying the document containing the symbol to be renamed, as well as
* the position.
* @returns A {Promise} containing either:
*
* - A {Range} of the string to rename and optionally a `placeholder` text of the string content to be renamed.
* - `{ defaultBehavior: boolean }` is returned (since 3.16) if the rename position is valid and the client should use
* its default behavior to compute the rename range.
* - `null` is returned when it is deemed that a ‘textDocument/rename’ request is not valid at the given position
*/
public prepareRename(
params: lsp.PrepareRenameParams
): Promise<lsp.Range | { range: lsp.Range; placeholder: string } | { defaultBehavior: boolean } | null> {
return this._sendRequest("textDocument/prepareRename", params)
}

/**
* Public: Send a `textDocument/rename` request.
*
Expand Down
2 changes: 2 additions & 0 deletions lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Logger, ConsoleLogger, FilteredLogger } from "./logger"
import DownloadFile from "./download-file"
import LinterPushV2Adapter from "./adapters/linter-push-v2-adapter"
import CommandExecutionAdapter from "./adapters/command-execution-adapter"
import RenameAdapter from "./adapters/rename-adapter"
export { getExePath } from "./utils"

export * from "./auto-languageclient"
Expand All @@ -21,4 +22,5 @@ export {
DownloadFile,
LinterPushV2Adapter,
CommandExecutionAdapter,
RenameAdapter,
}
35 changes: 35 additions & 0 deletions lib/views/dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { TextEditor } from "atom"

export default class Dialog {
public static async prompt(message: string): Promise<string> {
const miniEditor = new TextEditor({ mini: true })
const editorElement = atom.views.getView(miniEditor)

const messageElement = document.createElement("div")
messageElement.classList.add("message")
messageElement.textContent = message

const element = document.createElement("div")
element.classList.add("prompt")
element.appendChild(editorElement)
element.appendChild(messageElement)

const panel = atom.workspace.addModalPanel({
item: element,
visible: true,
})

editorElement.focus()

return new Promise((resolve, reject) => {
atom.commands.add(editorElement, "core:confirm", () => {
resolve(miniEditor.getText())
panel.destroy()
})
atom.commands.add(editorElement, "core:cancel", () => {
reject()
panel.destroy()
})
})
}
}