languageServerTools.ts•9.73 kB
import * as vscode from 'vscode';
import { Logger } from './logger';
export class LanguageServerTools {
constructor(private logger: Logger) {}
async getSymbols(uriString: string) {
try {
const uri = vscode.Uri.parse(uriString);
this.logger.debug(`Getting symbols for ${uri.fsPath}`);
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
'vscode.executeDocumentSymbolProvider',
uri
);
if (!symbols) {
return { symbols: [] };
}
// Convert to serializable format
const serializableSymbols = symbols.map(sym => this.serializeSymbol(sym));
this.logger.debug(`Found ${symbols.length} top-level symbols`);
return { symbols: serializableSymbols };
} catch (error) {
this.logger.error('Error getting symbols:', error);
throw new Error(`Failed to get symbols: ${error}`);
}
}
async findReferences(uriString: string, line: number, character: number) {
try {
const uri = vscode.Uri.parse(uriString);
const position = new vscode.Position(line, character);
this.logger.debug(`Finding references at ${uri.fsPath}:${line}:${character}`);
const locations = await vscode.commands.executeCommand<vscode.Location[]>(
'vscode.executeReferenceProvider',
uri,
position
);
if (!locations) {
return { references: [] };
}
const references = locations.map(loc => ({
uri: loc.uri.toString(),
range: {
start: {
line: loc.range.start.line,
character: loc.range.start.character
},
end: {
line: loc.range.end.line,
character: loc.range.end.character
}
}
}));
this.logger.debug(`Found ${references.length} references`);
return { references };
} catch (error) {
this.logger.error('Error finding references:', error);
throw new Error(`Failed to find references: ${error}`);
}
}
async rename(uriString: string, line: number, character: number, newName: string) {
try {
const uri = vscode.Uri.parse(uriString);
const position = new vscode.Position(line, character);
this.logger.debug(`Renaming symbol at ${uri.fsPath}:${line}:${character} to ${newName}`);
const workspaceEdit = await vscode.commands.executeCommand<vscode.WorkspaceEdit>(
'vscode.executeDocumentRenameProvider',
uri,
position,
newName
);
if (!workspaceEdit) {
return {
success: false,
message: 'No workspace edit returned'
};
}
// Apply the edit
const success = await vscode.workspace.applyEdit(workspaceEdit);
const changesCount = workspaceEdit.size;
const changes: any[] = [];
// Collect all changes for reporting
workspaceEdit.entries().forEach(([uri, edits]) => {
edits.forEach(edit => {
changes.push({
uri: uri.toString(),
range: {
start: {
line: edit.range.start.line,
character: edit.range.start.character
},
end: {
line: edit.range.end.line,
character: edit.range.end.character
}
},
oldText: '', // Not available from WorkspaceEdit
newText: edit.newText
});
});
});
this.logger.info(`Renamed ${changesCount} occurrences to ${newName}`);
return {
success,
changesCount,
changes
};
} catch (error) {
this.logger.error('Error renaming symbol:', error);
throw new Error(`Failed to rename symbol: ${error}`);
}
}
async getDefinition(uriString: string, line: number, character: number) {
try {
const uri = vscode.Uri.parse(uriString);
const position = new vscode.Position(line, character);
this.logger.debug(`Getting definition at ${uri.fsPath}:${line}:${character}`);
const locations = await vscode.commands.executeCommand<
vscode.Location[] | vscode.LocationLink[]
>(
'vscode.executeDefinitionProvider',
uri,
position
);
if (!locations || locations.length === 0) {
return { definitions: [] };
}
const definitions = locations.map(loc => {
if ('targetUri' in loc) {
// LocationLink
return {
uri: loc.targetUri.toString(),
range: {
start: {
line: loc.targetRange.start.line,
character: loc.targetRange.start.character
},
end: {
line: loc.targetRange.end.line,
character: loc.targetRange.end.character
}
}
};
} else {
// Location
return {
uri: loc.uri.toString(),
range: {
start: {
line: loc.range.start.line,
character: loc.range.start.character
},
end: {
line: loc.range.end.line,
character: loc.range.end.character
}
}
};
}
});
this.logger.debug(`Found ${definitions.length} definitions`);
return { definitions };
} catch (error) {
this.logger.error('Error getting definition:', error);
throw new Error(`Failed to get definition: ${error}`);
}
}
async getHover(uriString: string, line: number, character: number) {
try {
const uri = vscode.Uri.parse(uriString);
const position = new vscode.Position(line, character);
this.logger.debug(`Getting hover info at ${uri.fsPath}:${line}:${character}`);
const hovers = await vscode.commands.executeCommand<vscode.Hover[]>(
'vscode.executeHoverProvider',
uri,
position
);
if (!hovers || hovers.length === 0) {
return { hover: null };
}
// Take first hover
const hover = hovers[0];
const contents = hover.contents.map(content => {
if (typeof content === 'string') {
return { kind: 'plaintext', value: content };
} else {
return {
kind: 'markdown',
value: content.value
};
}
});
this.logger.debug('Found hover information');
return {
hover: {
contents,
range: hover.range ? {
start: {
line: hover.range.start.line,
character: hover.range.start.character
},
end: {
line: hover.range.end.line,
character: hover.range.end.character
}
} : null
}
};
} catch (error) {
this.logger.error('Error getting hover:', error);
throw new Error(`Failed to get hover info: ${error}`);
}
}
private serializeSymbol(symbol: vscode.DocumentSymbol): any {
return {
name: symbol.name,
detail: symbol.detail,
kind: vscode.SymbolKind[symbol.kind],
range: {
start: {
line: symbol.range.start.line,
character: symbol.range.start.character
},
end: {
line: symbol.range.end.line,
character: symbol.range.end.character
}
},
selectionRange: {
start: {
line: symbol.selectionRange.start.line,
character: symbol.selectionRange.start.character
},
end: {
line: symbol.selectionRange.end.line,
character: symbol.selectionRange.end.character
}
},
children: symbol.children?.map(child => this.serializeSymbol(child)) || []
};
}
}