Skip to content

Commit

Permalink
refactor(vscode): separate the workspace lsp server request from the …
Browse files Browse the repository at this point in the history
…chat feature
  • Loading branch information
Sma1lboy committed Oct 13, 2024
1 parent 798b173 commit 90b5354
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 111 deletions.
113 changes: 2 additions & 111 deletions clients/vscode/src/lsp/ChatFeature.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import { EventEmitter } from "events";
import {
window,
workspace,
Range,
Position,
Disposable,
CancellationToken,
TextEditorEdit,
TextDocument,
} from "vscode";
import { BaseLanguageClient, DynamicFeature, FeatureState, RegistrationData, TextEdit } from "vscode-languageclient";
import { Disposable, CancellationToken } from "vscode";
import { BaseLanguageClient, DynamicFeature, FeatureState, RegistrationData } from "vscode-languageclient";
import {
ServerCapabilities,
ChatFeatureRegistration,
Expand All @@ -24,14 +15,9 @@ import {
ChatEditToken,
ChatEditResolveRequest,
ChatEditResolveParams,
ApplyWorkspaceEditParams,
ApplyWorkspaceEditRequest,
SmartApplyCodeParams,
SmartApplyCodeRequest,
RevealEditorRangeRequest,
RevealEditorRangeParams,
} from "tabby-agent";
import { diffLines } from "diff";

export class ChatFeature extends EventEmitter implements DynamicFeature<unknown> {
private registration: string | undefined = undefined;
Expand Down Expand Up @@ -63,15 +49,6 @@ export class ChatFeature extends EventEmitter implements DynamicFeature<unknown>
if (capabilities.tabby?.chat) {
this.register({ id: this.registrationType.method, registerOptions: {} });
}

this.disposables.push(
this.client.onRequest(ApplyWorkspaceEditRequest.type, (params: ApplyWorkspaceEditParams) => {
return this.handleApplyWorkspaceEdit(params);
}),
this.client.onRequest(RevealEditorRangeRequest.type, (params: RevealEditorRangeParams) => {
return this.handleRevealEditorRange(params);
}),
);
}

register(data: RegistrationData<unknown>): void {
Expand Down Expand Up @@ -142,93 +119,7 @@ export class ChatFeature extends EventEmitter implements DynamicFeature<unknown>
return res;
}

private async handleApplyWorkspaceEdit(params: ApplyWorkspaceEditParams): Promise<boolean> {
const { edit, options } = params;
const activeEditor = window.activeTextEditor;
if (!activeEditor) {
return false;
}

try {
const success = await activeEditor.edit(
(editBuilder: TextEditorEdit) => {
Object.entries(edit.changes || {}).forEach(([uri, textEdits]) => {
const document = workspace.textDocuments.find((doc) => doc.uri.toString() === uri);
if (document && document === activeEditor.document) {
const textEdit = textEdits[0];
if (textEdits.length === 1 && textEdit) {
applyTextEditMinimalLineChange(editBuilder, textEdit, document);
} else {
textEdits.forEach((textEdit) => {
const range = new Range(
new Position(textEdit.range.start.line, textEdit.range.start.character),
new Position(textEdit.range.end.line, textEdit.range.end.character),
);
editBuilder.replace(range, textEdit.newText);
});
}
}
});
},
{
undoStopBefore: options?.undoStopBefore ?? false,
undoStopAfter: options?.undoStopAfter ?? false,
},
);

return success;
} catch (error) {
return false;
}
}

handleRevealEditorRange(params: RevealEditorRangeParams): boolean {
const { range, revealType } = params;
const activeEditor = window.activeTextEditor;
if (!activeEditor) {
return false;
}

activeEditor.revealRange(
new Range(
new Position(range.start.line, range.start.character),
new Position(range.end.line, range.end.character),
),
revealType,
);

return true;
}

async resolveEdit(params: ChatEditResolveParams): Promise<boolean> {
return this.client.sendRequest(ChatEditResolveRequest.method, params);
}
}

function applyTextEditMinimalLineChange(editBuilder: TextEditorEdit, textEdit: TextEdit, document: TextDocument) {
const documentRange = new Range(
new Position(textEdit.range.start.line, textEdit.range.start.character),
new Position(textEdit.range.end.line, textEdit.range.end.character),
);

const text = document.getText(documentRange);
const newText = textEdit.newText;
const diffs = diffLines(text, newText);

let line = documentRange.start.line;
for (const diff of diffs) {
if (!diff.count) {
continue;
}

if (diff.added) {
editBuilder.insert(new Position(line, 0), diff.value);
} else if (diff.removed) {
const range = new Range(new Position(line + 0, 0), new Position(line + diff.count, 0));
editBuilder.delete(range);
line += diff.count;
} else {
line += diff.count;
}
}
}
4 changes: 4 additions & 0 deletions clients/vscode/src/lsp/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,25 @@ import { Config } from "../Config";
import { InlineCompletionProvider } from "../InlineCompletionProvider";
import { GitProvider } from "../git/GitProvider";
import { getLogger } from "../logger";
import { WorkSpaceFeature } from "./WorkspaceFeature";

export class Client {
private readonly logger = getLogger("");
readonly agent: AgentFeature;
readonly chat: ChatFeature;
readonly telemetry: TelemetryFeature;
readonly workspace: WorkSpaceFeature;
constructor(
private readonly context: ExtensionContext,
readonly languageClient: BaseLanguageClient,
) {
this.agent = new AgentFeature(this.languageClient);
this.chat = new ChatFeature(this.languageClient);
this.workspace = new WorkSpaceFeature(this.languageClient);
this.telemetry = new TelemetryFeature(this.languageClient);
this.languageClient.registerFeature(this.agent);
this.languageClient.registerFeature(this.chat);
this.languageClient.registerFeature(this.workspace);
this.languageClient.registerFeature(this.telemetry);
this.languageClient.registerFeature(new DataStoreFeature(this.context, this.languageClient));
this.languageClient.registerFeature(new EditorOptionsFeature(this.languageClient));
Expand Down
152 changes: 152 additions & 0 deletions clients/vscode/src/lsp/WorkspaceFeature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import EventEmitter from "events";
import {
ApplyWorkspaceEditParams,
ApplyWorkspaceEditRequest,
RevealEditorRangeParams,
RevealEditorRangeRequest,
} from "tabby-agent";
import { BaseLanguageClient, FeatureState, RegistrationData, StaticFeature, TextEdit } from "vscode-languageclient";
import { Disposable, Position, Range, TextDocument, TextEditorEdit, window, workspace } from "vscode";
import { diffLines } from "diff";
import { getLogger } from "../logger";

Check failure on line 11 in clients/vscode/src/lsp/WorkspaceFeature.ts

View workflow job for this annotation

GitHub Actions / tests

'getLogger' is defined but never used

Check failure on line 11 in clients/vscode/src/lsp/WorkspaceFeature.ts

View workflow job for this annotation

GitHub Actions / autofix

'getLogger' is defined but never used
export class WorkSpaceFeature extends EventEmitter implements StaticFeature {
private registration: string | undefined = undefined;
private disposables: Disposable[] = [];

constructor(private readonly client: BaseLanguageClient) {
super();
}
getState(): FeatureState {
return { kind: "static" };
}

fillInitializeParams() {
// nothing
}

fillClientCapabilities(): void {
// nothing
}

preInitialize(): void {
// nothing
}

initialize(): void {
this.disposables.push(
this.client.onRequest(ApplyWorkspaceEditRequest.type, (params: ApplyWorkspaceEditParams) => {
return this.handleApplyWorkspaceEdit(params);
}),
this.client.onRequest(RevealEditorRangeRequest.type, (params: RevealEditorRangeParams) => {
return this.handleRevealEditorRange(params);
}),
);
}

register(data: RegistrationData<unknown>): void {
this.registration = data.id;
this.emit("didChangeAvailability", true);
}

unregister(id: string): void {
if (this.registration === id) {
this.registration = undefined;
this.emit("didChangeAvailability", false);
}
}

clear(): void {
this.disposables.forEach((disposable) => disposable.dispose());
this.disposables = [];
}

get isAvailable(): boolean {
return !!this.registration;
}

private async handleApplyWorkspaceEdit(params: ApplyWorkspaceEditParams): Promise<boolean> {
const { edit, options } = params;
const activeEditor = window.activeTextEditor;
if (!activeEditor) {
return false;
}

try {
const success = await activeEditor.edit(
(editBuilder: TextEditorEdit) => {
Object.entries(edit.changes || {}).forEach(([uri, textEdits]) => {
const document = workspace.textDocuments.find((doc) => doc.uri.toString() === uri);
if (document && document === activeEditor.document) {
const textEdit = textEdits[0];
if (textEdits.length === 1 && textEdit) {
applyTextEditMinimalLineChange(editBuilder, textEdit, document);
} else {
textEdits.forEach((textEdit) => {
const range = new Range(
new Position(textEdit.range.start.line, textEdit.range.start.character),
new Position(textEdit.range.end.line, textEdit.range.end.character),
);
editBuilder.replace(range, textEdit.newText);
});
}
}
});
},
{
undoStopBefore: options?.undoStopBefore ?? false,
undoStopAfter: options?.undoStopAfter ?? false,
},
);

return success;
} catch (error) {
return false;
}
}

handleRevealEditorRange(params: RevealEditorRangeParams): boolean {
const { range, revealType } = params;
const activeEditor = window.activeTextEditor;
if (!activeEditor) {
return false;
}

activeEditor.revealRange(
new Range(
new Position(range.start.line, range.start.character),
new Position(range.end.line, range.end.character),
),
revealType,
);

return true;
}
}

function applyTextEditMinimalLineChange(editBuilder: TextEditorEdit, textEdit: TextEdit, document: TextDocument) {
const documentRange = new Range(
new Position(textEdit.range.start.line, textEdit.range.start.character),
new Position(textEdit.range.end.line, textEdit.range.end.character),
);

const text = document.getText(documentRange);
const newText = textEdit.newText;
const diffs = diffLines(text, newText);

let line = documentRange.start.line;
for (const diff of diffs) {
if (!diff.count) {
continue;
}

if (diff.added) {
editBuilder.insert(new Position(line, 0), diff.value);
} else if (diff.removed) {
const range = new Range(new Position(line + 0, 0), new Position(line + diff.count, 0));
editBuilder.delete(range);
line += diff.count;
} else {
line += diff.count;
}
}
}

0 comments on commit 90b5354

Please sign in to comment.