Language Services API
The Language Services API provides language-specific features like syntax highlighting, diagnostics, hover information, and intelligent code analysis.
Overview
The Language Services API enables you to:
- Provide syntax highlighting and tokenization
- Generate diagnostic information (errors, warnings)
- Offer hover information and documentation
- Implement go-to-definition and find references
- Support code formatting and refactoring
- Create language-specific completion providers
- Handle semantic analysis and type checking
Basic Usage
Registering Language Features
typescript
import { TraeAPI } from '@trae/api';
// Register a hover provider
const hoverProvider = TraeAPI.languages.registerHoverProvider(
{ scheme: 'file', language: 'typescript' },
{
async provideHover(document, position, token) {
const wordRange = document.getWordRangeAtPosition(position);
if (!wordRange) {
return;
}
const word = document.getText(wordRange);
const hoverContent = await getHoverInformation(word, document, position);
if (hoverContent) {
return new TraeAPI.Hover(hoverContent, wordRange);
}
}
}
);
// Register a definition provider
const definitionProvider = TraeAPI.languages.registerDefinitionProvider(
{ scheme: 'file', language: 'typescript' },
{
async provideDefinition(document, position, token) {
const wordRange = document.getWordRangeAtPosition(position);
if (!wordRange) {
return;
}
const word = document.getText(wordRange);
const definitions = await findDefinitions(word, document, position);
return definitions.map(def => new TraeAPI.Location(
TraeAPI.Uri.file(def.file),
new TraeAPI.Position(def.line, def.character)
));
}
}
);
async function getHoverInformation(word: string, document: TraeAPI.TextDocument, position: TraeAPI.Position): Promise<TraeAPI.MarkdownString | undefined> {
// Implementation would analyze the code and return hover information
const markdown = new TraeAPI.MarkdownString();
markdown.appendCodeblock(`function ${word}(): void`, 'typescript');
markdown.appendMarkdown('\n\nThis is a TypeScript function.');
return markdown;
}
async function findDefinitions(word: string, document: TraeAPI.TextDocument, position: TraeAPI.Position): Promise<Array<{ file: string; line: number; character: number }>> {
// Implementation would find all definitions of the symbol
return [
{ file: document.fileName, line: 10, character: 5 }
];
}Diagnostic Provider
typescript
class DiagnosticProvider {
private diagnosticCollection: TraeAPI.DiagnosticCollection;
constructor() {
this.diagnosticCollection = TraeAPI.languages.createDiagnosticCollection('myLanguage');
this.setupDocumentListener();
}
private setupDocumentListener() {
// Listen for document changes and provide diagnostics
TraeAPI.workspace.onDidChangeTextDocument(event => {
this.updateDiagnostics(event.document);
});
TraeAPI.workspace.onDidOpenTextDocument(document => {
this.updateDiagnostics(document);
});
}
private async updateDiagnostics(document: TraeAPI.TextDocument) {
if (document.languageId !== 'typescript') {
return;
}
const diagnostics = await this.analyzDocument(document);
this.diagnosticCollection.set(document.uri, diagnostics);
}
private async analyzDocument(document: TraeAPI.TextDocument): Promise<TraeAPI.Diagnostic[]> {
const diagnostics: TraeAPI.Diagnostic[] = [];
const text = document.getText();
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for syntax errors
if (line.includes('console.log') && !line.includes(';')) {
const range = new TraeAPI.Range(
new TraeAPI.Position(i, line.indexOf('console.log')),
new TraeAPI.Position(i, line.length)
);
const diagnostic = new TraeAPI.Diagnostic(
range,
'Missing semicolon',
TraeAPI.DiagnosticSeverity.Warning
);
diagnostic.code = 'missing-semicolon';
diagnostic.source = 'myLanguageServer';
diagnostics.push(diagnostic);
}
// Check for unused variables
const variableMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
if (variableMatch) {
const variableName = variableMatch[1];
const isUsed = this.isVariableUsed(variableName, text, i);
if (!isUsed) {
const range = new TraeAPI.Range(
new TraeAPI.Position(i, variableMatch.index!),
new TraeAPI.Position(i, variableMatch.index! + variableMatch[0].length)
);
const diagnostic = new TraeAPI.Diagnostic(
range,
`Variable '${variableName}' is declared but never used`,
TraeAPI.DiagnosticSeverity.Information
);
diagnostic.code = 'unused-variable';
diagnostic.tags = [TraeAPI.DiagnosticTag.Unnecessary];
diagnostics.push(diagnostic);
}
}
// Check for deprecated APIs
if (line.includes('oldFunction')) {
const range = new TraeAPI.Range(
new TraeAPI.Position(i, line.indexOf('oldFunction')),
new TraeAPI.Position(i, line.indexOf('oldFunction') + 'oldFunction'.length)
);
const diagnostic = new TraeAPI.Diagnostic(
range,
'oldFunction is deprecated. Use newFunction instead.',
TraeAPI.DiagnosticSeverity.Warning
);
diagnostic.tags = [TraeAPI.DiagnosticTag.Deprecated];
diagnostics.push(diagnostic);
}
}
return diagnostics;
}
private isVariableUsed(variableName: string, text: string, declarationLine: number): boolean {
const lines = text.split('\n');
// Check lines after declaration
for (let i = declarationLine + 1; i < lines.length; i++) {
if (lines[i].includes(variableName)) {
return true;
}
}
return false;
}
dispose() {
this.diagnosticCollection.dispose();
}
}
// Initialize diagnostic provider
const diagnosticProvider = new DiagnosticProvider();Advanced Language Features
Code Actions Provider
typescript
class CodeActionsProvider implements TraeAPI.CodeActionProvider {
async provideCodeActions(
document: TraeAPI.TextDocument,
range: TraeAPI.Range,
context: TraeAPI.CodeActionContext,
token: TraeAPI.CancellationToken
): Promise<TraeAPI.CodeAction[]> {
const actions: TraeAPI.CodeAction[] = [];
// Quick fixes for diagnostics
for (const diagnostic of context.diagnostics) {
if (diagnostic.code === 'missing-semicolon') {
const fixAction = new TraeAPI.CodeAction(
'Add semicolon',
TraeAPI.CodeActionKind.QuickFix
);
fixAction.edit = new TraeAPI.WorkspaceEdit();
fixAction.edit.insert(
document.uri,
diagnostic.range.end,
';'
);
fixAction.diagnostics = [diagnostic];
actions.push(fixAction);
}
if (diagnostic.code === 'unused-variable') {
const removeAction = new TraeAPI.CodeAction(
'Remove unused variable',
TraeAPI.CodeActionKind.QuickFix
);
removeAction.edit = new TraeAPI.WorkspaceEdit();
const line = document.lineAt(diagnostic.range.start.line);
removeAction.edit.delete(
document.uri,
line.rangeIncludingLineBreak
);
removeAction.diagnostics = [diagnostic];
actions.push(removeAction);
}
}
// Refactoring actions
const selectedText = document.getText(range);
if (selectedText && !range.isEmpty) {
// Extract to function
const extractAction = new TraeAPI.CodeAction(
'Extract to function',
TraeAPI.CodeActionKind.Refactor
);
extractAction.edit = await this.createExtractFunctionEdit(document, range, selectedText);
actions.push(extractAction);
// Extract to variable
const extractVarAction = new TraeAPI.CodeAction(
'Extract to variable',
TraeAPI.CodeActionKind.Refactor
);
extractVarAction.edit = await this.createExtractVariableEdit(document, range, selectedText);
actions.push(extractVarAction);
}
// Source actions
const organizeImportsAction = new TraeAPI.CodeAction(
'Organize imports',
TraeAPI.CodeActionKind.SourceOrganizeImports
);
organizeImportsAction.edit = await this.createOrganizeImportsEdit(document);
actions.push(organizeImportsAction);
return actions;
}
private async createExtractFunctionEdit(
document: TraeAPI.TextDocument,
range: TraeAPI.Range,
selectedText: string
): Promise<TraeAPI.WorkspaceEdit> {
const edit = new TraeAPI.WorkspaceEdit();
const functionName = 'extractedFunction';
// Replace selected text with function call
edit.replace(document.uri, range, `${functionName}()`);
// Add function definition
const functionDef = `\nfunction ${functionName}() {\n ${selectedText}\n}\n`;
edit.insert(document.uri, new TraeAPI.Position(0, 0), functionDef);
return edit;
}
private async createExtractVariableEdit(
document: TraeAPI.TextDocument,
range: TraeAPI.Range,
selectedText: string
): Promise<TraeAPI.WorkspaceEdit> {
const edit = new TraeAPI.WorkspaceEdit();
const variableName = 'extractedVariable';
// Replace selected text with variable reference
edit.replace(document.uri, range, variableName);
// Add variable declaration
const variableDef = `const ${variableName} = ${selectedText};\n`;
edit.insert(document.uri, new TraeAPI.Position(range.start.line, 0), variableDef);
return edit;
}
private async createOrganizeImportsEdit(document: TraeAPI.TextDocument): Promise<TraeAPI.WorkspaceEdit> {
const edit = new TraeAPI.WorkspaceEdit();
const text = document.getText();
const lines = text.split('\n');
// Find and organize import statements
const imports: string[] = [];
const nonImportLines: string[] = [];
for (const line of lines) {
if (line.trim().startsWith('import ')) {
imports.push(line);
} else {
nonImportLines.push(line);
}
}
// Sort imports
imports.sort();
// Reconstruct file content
const newContent = [...imports, '', ...nonImportLines].join('\n');
edit.replace(
document.uri,
new TraeAPI.Range(0, 0, document.lineCount, 0),
newContent
);
return edit;
}
}
// Register code actions provider
const codeActionsProvider = new CodeActionsProvider();
TraeAPI.languages.registerCodeActionsProvider(
{ scheme: 'file', language: 'typescript' },
codeActionsProvider
);Document Formatting Provider
typescript
class FormattingProvider implements TraeAPI.DocumentFormattingEditProvider {
async provideDocumentFormattingEdits(
document: TraeAPI.TextDocument,
options: TraeAPI.FormattingOptions,
token: TraeAPI.CancellationToken
): Promise<TraeAPI.TextEdit[]> {
const edits: TraeAPI.TextEdit[] = [];
const text = document.getText();
const formattedText = await this.formatCode(text, options);
if (formattedText !== text) {
const fullRange = new TraeAPI.Range(
0, 0,
document.lineCount, 0
);
edits.push(TraeAPI.TextEdit.replace(fullRange, formattedText));
}
return edits;
}
async provideDocumentRangeFormattingEdits(
document: TraeAPI.TextDocument,
range: TraeAPI.Range,
options: TraeAPI.FormattingOptions,
token: TraeAPI.CancellationToken
): Promise<TraeAPI.TextEdit[]> {
const edits: TraeAPI.TextEdit[] = [];
const rangeText = document.getText(range);
const formattedText = await this.formatCode(rangeText, options);
if (formattedText !== rangeText) {
edits.push(TraeAPI.TextEdit.replace(range, formattedText));
}
return edits;
}
private async formatCode(code: string, options: TraeAPI.FormattingOptions): Promise<string> {
// Simple formatting implementation
let formatted = code;
// Fix indentation
const lines = formatted.split('\n');
let indentLevel = 0;
const indentString = options.insertSpaces ? ' '.repeat(options.tabSize) : '\t';
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.endsWith('{')) {
lines[i] = indentString.repeat(indentLevel) + line;
indentLevel++;
} else if (line.startsWith('}')) {
indentLevel = Math.max(0, indentLevel - 1);
lines[i] = indentString.repeat(indentLevel) + line;
} else if (line) {
lines[i] = indentString.repeat(indentLevel) + line;
}
}
formatted = lines.join('\n');
// Add missing semicolons
formatted = formatted.replace(/([^;{}])\n/g, '$1;\n');
// Fix spacing around operators
formatted = formatted.replace(/([a-zA-Z0-9])([=+\-*/])([a-zA-Z0-9])/g, '$1 $2 $3');
return formatted;
}
}
// Register formatting providers
const formattingProvider = new FormattingProvider();
TraeAPI.languages.registerDocumentFormattingEditProvider(
{ scheme: 'file', language: 'typescript' },
formattingProvider
);
TraeAPI.languages.registerDocumentRangeFormattingEditProvider(
{ scheme: 'file', language: 'typescript' },
formattingProvider
);Symbol Provider
typescript
class DocumentSymbolProvider implements TraeAPI.DocumentSymbolProvider {
async provideDocumentSymbols(
document: TraeAPI.TextDocument,
token: TraeAPI.CancellationToken
): Promise<TraeAPI.DocumentSymbol[]> {
const symbols: TraeAPI.DocumentSymbol[] = [];
const text = document.getText();
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Find functions
const functionMatch = line.match(/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(([^)]*)\)/);
if (functionMatch) {
const name = functionMatch[1];
const params = functionMatch[2];
const symbol = new TraeAPI.DocumentSymbol(
name,
`(${params})`,
TraeAPI.SymbolKind.Function,
new TraeAPI.Range(i, 0, i, line.length),
new TraeAPI.Range(i, functionMatch.index!, i, functionMatch.index! + functionMatch[0].length)
);
symbols.push(symbol);
}
// Find classes
const classMatch = line.match(/class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
if (classMatch) {
const name = classMatch[1];
const symbol = new TraeAPI.DocumentSymbol(
name,
'',
TraeAPI.SymbolKind.Class,
new TraeAPI.Range(i, 0, i, line.length),
new TraeAPI.Range(i, classMatch.index!, i, classMatch.index! + classMatch[0].length)
);
// Find class methods
const methods = this.findClassMethods(lines, i);
symbol.children = methods;
symbols.push(symbol);
}
// Find variables
const variableMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
if (variableMatch) {
const name = variableMatch[1];
const symbol = new TraeAPI.DocumentSymbol(
name,
'',
TraeAPI.SymbolKind.Variable,
new TraeAPI.Range(i, 0, i, line.length),
new TraeAPI.Range(i, variableMatch.index!, i, variableMatch.index! + variableMatch[0].length)
);
symbols.push(symbol);
}
}
return symbols;
}
private findClassMethods(lines: string[], classStartLine: number): TraeAPI.DocumentSymbol[] {
const methods: TraeAPI.DocumentSymbol[] = [];
let braceCount = 0;
let inClass = false;
for (let i = classStartLine; i < lines.length; i++) {
const line = lines[i];
if (line.includes('{')) {
braceCount++;
inClass = true;
}
if (line.includes('}')) {
braceCount--;
if (braceCount === 0) {
break;
}
}
if (inClass && braceCount === 1) {
const methodMatch = line.match(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(([^)]*)\)\s*{/);
if (methodMatch) {
const name = methodMatch[1];
const params = methodMatch[2];
const method = new TraeAPI.DocumentSymbol(
name,
`(${params})`,
TraeAPI.SymbolKind.Method,
new TraeAPI.Range(i, 0, i, line.length),
new TraeAPI.Range(i, methodMatch.index!, i, methodMatch.index! + methodMatch[0].length)
);
methods.push(method);
}
}
}
return methods;
}
}
// Register symbol provider
const symbolProvider = new DocumentSymbolProvider();
TraeAPI.languages.registerDocumentSymbolProvider(
{ scheme: 'file', language: 'typescript' },
symbolProvider
);Reference Provider
typescript
class ReferenceProvider implements TraeAPI.ReferenceProvider {
async provideReferences(
document: TraeAPI.TextDocument,
position: TraeAPI.Position,
context: TraeAPI.ReferenceContext,
token: TraeAPI.CancellationToken
): Promise<TraeAPI.Location[]> {
const wordRange = document.getWordRangeAtPosition(position);
if (!wordRange) {
return [];
}
const word = document.getText(wordRange);
const references: TraeAPI.Location[] = [];
// Search in current document
const currentDocRefs = this.findReferencesInDocument(document, word);
references.push(...currentDocRefs);
// Search in workspace
const workspaceRefs = await this.findReferencesInWorkspace(word, document.uri);
references.push(...workspaceRefs);
// Include declaration if requested
if (context.includeDeclaration) {
const declaration = await this.findDeclaration(word, document);
if (declaration) {
references.push(declaration);
}
}
return references;
}
private findReferencesInDocument(document: TraeAPI.TextDocument, word: string): TraeAPI.Location[] {
const references: TraeAPI.Location[] = [];
const text = document.getText();
const regex = new RegExp(`\\b${word}\\b`, 'g');
let match;
while ((match = regex.exec(text)) !== null) {
const position = document.positionAt(match.index);
const range = new TraeAPI.Range(
position,
document.positionAt(match.index + word.length)
);
references.push(new TraeAPI.Location(document.uri, range));
}
return references;
}
private async findReferencesInWorkspace(word: string, excludeUri: TraeAPI.Uri): Promise<TraeAPI.Location[]> {
const references: TraeAPI.Location[] = [];
// Find all TypeScript files in workspace
const files = await TraeAPI.workspace.findFiles('**/*.{ts,js}', '**/node_modules/**');
for (const file of files) {
if (file.toString() === excludeUri.toString()) {
continue;
}
try {
const document = await TraeAPI.workspace.openTextDocument(file);
const fileRefs = this.findReferencesInDocument(document, word);
references.push(...fileRefs);
} catch (error) {
console.error('Error reading file:', file.toString(), error);
}
}
return references;
}
private async findDeclaration(word: string, document: TraeAPI.TextDocument): Promise<TraeAPI.Location | undefined> {
const text = document.getText();
const declarationRegex = new RegExp(`(?:function|class|const|let|var)\\s+${word}\\b`);
const match = declarationRegex.exec(text);
if (match) {
const position = document.positionAt(match.index);
const range = new TraeAPI.Range(
position,
document.positionAt(match.index + match[0].length)
);
return new TraeAPI.Location(document.uri, range);
}
return undefined;
}
}
// Register reference provider
const referenceProvider = new ReferenceProvider();
TraeAPI.languages.registerReferenceProvider(
{ scheme: 'file', language: 'typescript' },
referenceProvider
);Language Server Integration
Custom Language Server
typescript
import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';
class CustomLanguageServer {
private client: LanguageClient | undefined;
async start() {
// Server options
const serverOptions: ServerOptions = {
command: 'node',
args: ['/path/to/language-server.js'],
options: {
env: {
...process.env,
NODE_ENV: 'production'
}
}
};
// Client options
const clientOptions: LanguageClientOptions = {
documentSelector: [
{ scheme: 'file', language: 'mylanguage' }
],
synchronize: {
fileEvents: TraeAPI.workspace.createFileSystemWatcher('**/*.mylang')
},
initializationOptions: {
settings: {
maxNumberOfProblems: 100,
enableCodeActions: true,
enableCompletion: true
}
}
};
// Create and start the client
this.client = new LanguageClient(
'myLanguageServer',
'My Language Server',
serverOptions,
clientOptions
);
await this.client.start();
console.log('Language server started');
}
async stop() {
if (this.client) {
await this.client.stop();
this.client = undefined;
console.log('Language server stopped');
}
}
async sendCustomRequest(method: string, params: any): Promise<any> {
if (!this.client) {
throw new Error('Language server not started');
}
return await this.client.sendRequest(method, params);
}
onCustomNotification(method: string, handler: (params: any) => void) {
if (!this.client) {
throw new Error('Language server not started');
}
this.client.onNotification(method, handler);
}
}
// Initialize language server
const languageServer = new CustomLanguageServer();
languageServer.start();Semantic Highlighting
Token Provider
typescript
class SemanticTokensProvider implements TraeAPI.DocumentSemanticTokensProvider {
private readonly legend: TraeAPI.SemanticTokensLegend;
constructor() {
this.legend = new TraeAPI.SemanticTokensLegend(
['class', 'function', 'variable', 'parameter', 'property', 'keyword'],
['declaration', 'definition', 'readonly', 'static', 'deprecated']
);
}
async provideDocumentSemanticTokens(
document: TraeAPI.TextDocument,
token: TraeAPI.CancellationToken
): Promise<TraeAPI.SemanticTokens> {
const builder = new TraeAPI.SemanticTokensBuilder(this.legend);
const text = document.getText();
const lines = text.split('\n');
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
const line = lines[lineIndex];
// Tokenize functions
const functionRegex = /\b(function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
let match;
while ((match = functionRegex.exec(line)) !== null) {
// 'function' keyword
builder.push(
lineIndex,
match.index,
match[1].length,
this.legend.tokenTypes.indexOf('keyword'),
0
);
// Function name
builder.push(
lineIndex,
match.index + match[1].length + 1,
match[2].length,
this.legend.tokenTypes.indexOf('function'),
this.encodeModifiers(['declaration'])
);
}
// Tokenize classes
const classRegex = /\b(class)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
while ((match = classRegex.exec(line)) !== null) {
// 'class' keyword
builder.push(
lineIndex,
match.index,
match[1].length,
this.legend.tokenTypes.indexOf('keyword'),
0
);
// Class name
builder.push(
lineIndex,
match.index + match[1].length + 1,
match[2].length,
this.legend.tokenTypes.indexOf('class'),
this.encodeModifiers(['declaration'])
);
}
// Tokenize variables
const variableRegex = /\b(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
while ((match = variableRegex.exec(line)) !== null) {
// Variable keyword
builder.push(
lineIndex,
match.index,
match[1].length,
this.legend.tokenTypes.indexOf('keyword'),
0
);
// Variable name
const modifiers = match[1] === 'const' ? ['readonly'] : [];
builder.push(
lineIndex,
match.index + match[1].length + 1,
match[2].length,
this.legend.tokenTypes.indexOf('variable'),
this.encodeModifiers(modifiers)
);
}
}
return builder.build();
}
private encodeModifiers(modifiers: string[]): number {
let result = 0;
for (const modifier of modifiers) {
const index = this.legend.tokenModifiers.indexOf(modifier);
if (index !== -1) {
result |= (1 << index);
}
}
return result;
}
}
// Register semantic tokens provider
const semanticTokensProvider = new SemanticTokensProvider();
TraeAPI.languages.registerDocumentSemanticTokensProvider(
{ scheme: 'file', language: 'typescript' },
semanticTokensProvider,
semanticTokensProvider.legend
);API Reference
Core Interfaces
typescript
interface HoverProvider {
provideHover(
document: TextDocument,
position: Position,
token: CancellationToken
): ProviderResult<Hover>;
}
interface DefinitionProvider {
provideDefinition(
document: TextDocument,
position: Position,
token: CancellationToken
): ProviderResult<Definition | DefinitionLink[]>;
}
interface CodeActionProvider {
provideCodeActions(
document: TextDocument,
range: Range,
context: CodeActionContext,
token: CancellationToken
): ProviderResult<CodeAction[]>;
}
interface DocumentFormattingEditProvider {
provideDocumentFormattingEdits(
document: TextDocument,
options: FormattingOptions,
token: CancellationToken
): ProviderResult<TextEdit[]>;
}
interface DiagnosticCollection {
readonly name: string;
set(uri: Uri, diagnostics: Diagnostic[]): void;
delete(uri: Uri): void;
clear(): void;
forEach(callback: (uri: Uri, diagnostics: Diagnostic[]) => void): void;
dispose(): void;
}Best Practices
- Performance: Cache analysis results and use incremental updates
- Accuracy: Provide precise ranges and positions for all features
- User Experience: Show progress for long-running operations
- Error Handling: Handle malformed code gracefully
- Memory Management: Dispose of providers and collections properly
- Consistency: Follow language-specific conventions and standards
- Extensibility: Design providers to be easily extended
- Testing: Thoroughly test with various code patterns
Related APIs
- Editor API - For editor integration
- Completion API - For code completion
- Workspace API - For file operations
- Commands API - For command integration