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
100 changes: 99 additions & 1 deletion lib/auto-languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ import { Socket } from "net"
import { LanguageClientConnection } from "./languageclient"
import { ConsoleLogger, FilteredLogger, Logger } from "./logger"
import { LanguageServerProcess, ServerManager, ActiveServer } 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'

export { ActiveServer, LanguageClientConnection, LanguageServerProcess }
export type ConnectionType = "stdio" | "socket" | "ipc"
Expand Down Expand Up @@ -218,6 +219,7 @@ export default class AutoLanguageClient {
dynamicRegistration: false,
},
rename: {
prepareSupport: true,
dynamicRegistration: false,
},
moniker: {
Expand Down Expand Up @@ -308,6 +310,7 @@ export default class AutoLanguageClient {
this.getServerName()
)
this._serverManager.startListening()
this.registerRenameCommands()
process.on("exit", () => this.exitCleanup.bind(this))
}

Expand Down Expand Up @@ -890,6 +893,101 @@ export default class AutoLanguageClient {
rename: this.getRename.bind(this),
}
}

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,
Expand Down
16 changes: 16 additions & 0 deletions lib/languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,22 @@ export class LanguageClientConnection extends EventEmitter {
return this._sendRequest("textDocument/onTypeFormatting", 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
}
37 changes: 37 additions & 0 deletions lib/views/dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { TextEditor } from "atom"

export default class Dialog {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually created a dialog view for some other atom packages https://github.com/UziTech/atom-modal-views. Do we want to use that instead of creating our own?

/cc @aminya

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually created a dialog view for some other atom packages UziTech/atom-modal-views. Do we want to use that instead of creating our own?

That sounds like a good plan! I love reusing code.

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()
})

})
}
}