Something went wrong. Try again.
fork of Gleam compiler for Lua backend forked from gleam.run/gleam
Something went wrong. Try again.
91 kB · 2438 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439// SPDX-License-Identifier: Apache-2.0// SPDX-FileCopyrightText: 2023 The Gleam contributors
use camino::Utf8PathBuf;use ecow::{EcoString, eco_format};use gleam_core::{ Error, Result, Warning, analyse::name::correct_name_case, ast::{ self, Constant, CustomType, DefinitionLocation, ModuleConstant, PatternUnusedArguments, TypedArg, TypedClauseGuard, TypedConstant, TypedExpr, TypedFunction, TypedModule, TypedPattern, TypedRecordConstructor, }, build::{ ExpressionPosition, Located, Module, UnqualifiedImport, type_constructor_from_modules, }, config::PackageConfig, io::{BeamCompilerIO, CommandExecutor, FileSystemReader, FileSystemWriter}, paths::ProjectPaths, type_::{ self, Deprecation, ModuleInterface, Type, TypeConstructor, ValueConstructor, ValueConstructorVariant, error::{Named, VariableSyntax}, printer::Printer, },};use itertools::Itertools;use lsp::CodeAction;use lsp_server::ResponseError;use lsp_types::{ self as lsp, Contents, DocumentSymbol, FoldingRange, FoldingRangeKind, Hover, MarkedString, MarkupContent, Position, PrepareRenameResult, Range, SignatureHelp, SymbolKind, SymbolTag, TextEdit, Uri as Url, WorkspaceEdit,};use src_span::{LineNumbers, SrcSpan};use std::{ collections::{HashMap, HashSet}, sync::Arc,};
use crate::{ code_action::{ ConvertIntToDifferentBase, DiscardUnusedVariable, RemoveRedundantRecordUpdate, ReplaceUnderscoreWithType, code_action_fix_deprecated_pipe, type_errors_for_module, }, reference::find_module_references_in_module, rename::{rename_module_alias, rename_module_occurrences, rename_type_variable},};
use super::{ DownloadDependencies, MakeLocker, code_action::{ AddAnnotations, AddMissingTypeParameter, AddOmittedLabels, AnnotateTopLevelDefinitions, CodeActionBuilder, CollapseNestedCase, ConvertBetweenDocAndRegularComment, ConvertFromUse, ConvertToFunctionCall, ConvertToPipe, ConvertToUse, CreateUnknownModule, ExpandFunctionCapture, ExtractConstant, ExtractFunction, ExtractVariable, FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation, FixTruncatedBitArraySegment, GenerateDynamicDecoder, GenerateFunction, GenerateJsonEncoder, GenerateVariant, InlineVariable, InterpolateString, LetAssertToCase, MergeCaseBranches, PatternMatchOnValue, RedundantTupleInCaseSubject, RemoveBlock, RemoveEchos, RemovePrivateOpaque, RemoveUnreachableCaseClauses, RemoveUnusedImports, UnwrapAnonymousFunction, UseLabelShorthandSyntax, WrapInAnonymousFunction, WrapInBlock, code_action_add_missing_patterns, code_action_convert_qualified_constructor_to_unqualified, code_action_convert_unqualified_constructor_to_qualified, code_action_generate_type, code_action_import_module, code_action_inexhaustive_let_to_case, }, compiler::LspProjectCompiler, completer::Completer, files::FileSystemProxy, progress::ProgressReporter, reference::{ FindVariableReferences, Referenced, VariableReferenceKind, find_label_references, find_label_references_in_module, find_module_references, reference_for_ast_node, }, rename::{ RenameOutcome, RenameTarget, Renamed, rename_label, rename_local_variable, rename_module_entity, }, signature_help, src_span_to_lsp_range,};
#[derive(Debug, PartialEq, Eq)]pub struct Response<T> { pub result: Result<T, Error>, pub warnings: Vec<Warning>, pub compilation: Compilation,}
#[derive(Debug, PartialEq, Eq)]pub enum Compilation { /// Compilation was attempted and succeeded for these modules. Yes(Vec<Utf8PathBuf>), /// Compilation was not attempted for this operation. No,}
#[derive(Debug)]pub struct LanguageServerEngine<IO, Reporter> { pub(crate) paths: ProjectPaths,
/// A compiler for the project that supports repeat compilation of the root /// package. /// In the event the project config changes this will need to be /// discarded and reloaded to handle any changes to dependencies. pub(crate) compiler: LspProjectCompiler<FileSystemProxy<IO>>,
modules_compiled_since_last_feedback: Vec<Utf8PathBuf>, compiled_since_last_feedback: bool, error: Option<Error>,
// Used to publish progress notifications to the client without waiting for // the usual request-response loop. progress_reporter: Reporter,
/// Used to know if to show the "View on HexDocs" link /// when hovering on an imported value hex_deps: HashSet<EcoString>,}
impl<'a, IO, Reporter> LanguageServerEngine<IO, Reporter>where // IO to be supplied from outside of gleam-core IO: FileSystemReader + FileSystemWriter + BeamCompilerIO + CommandExecutor + DownloadDependencies + MakeLocker + Clone, // IO to be supplied from inside of gleam-core Reporter: ProgressReporter + Clone + 'a,{ pub fn new( config: PackageConfig, progress_reporter: Reporter, io: FileSystemProxy<IO>, paths: ProjectPaths, ) -> Result<Self> { let locker = io.inner().make_locker(&paths, config.target)?;
// Download dependencies to ensure they are up-to-date for this new // configuration and new instance of the compiler progress_reporter.dependency_downloading_started(); let manifest = io.inner().download_dependencies(&paths); progress_reporter.dependency_downloading_finished();
// NOTE: This must come after the progress reporter has finished! let manifest = manifest?;
let compiler: LspProjectCompiler<FileSystemProxy<IO>> = LspProjectCompiler::new(manifest, config, paths.clone(), io.clone(), locker)?;
let hex_deps = compiler .project_compiler .packages .iter() .flat_map(|(k, v)| match &v.source { gleam_core::manifest::ManifestPackageSource::Hex { .. } => { Some(EcoString::from(k.as_str())) }
gleam_core::manifest::ManifestPackageSource::Git { .. } | gleam_core::manifest::ManifestPackageSource::Local { .. } => None, }) .collect();
Ok(Self { modules_compiled_since_last_feedback: vec![], compiled_since_last_feedback: false, progress_reporter, compiler, paths, error: None, hex_deps, }) }
pub fn compile_please(&mut self) -> Response<()> { self.respond(Self::compile) }
/// Compile the project if we are in one. Otherwise do nothing. fn compile(&mut self) -> Result<(), Error> { self.compiled_since_last_feedback = true;
self.progress_reporter.compilation_started(); let outcome = self.compiler.compile(); self.progress_reporter.compilation_finished();
let result = outcome // Register which modules have changed .map(|modules| self.modules_compiled_since_last_feedback.extend(modules)) // Return the error, if present .into_result();
self.error = match &result { Ok(_) => None, Err(error) => Some(error.clone()), };
result }
fn take_warnings(&mut self) -> Vec<Warning> { self.compiler.take_warnings() }
pub fn goto_definition( &mut self, params: lsp::DefinitionParams, ) -> Response<Option<lsp::Location>> { self.respond(|this| { let params = params.text_document_position_params; let (line_numbers, node) = match this.node_at_position(¶ms) { Some(location) => location, None => return Ok(None), };
let Some(location) = node.definition_location(this.compiler.project_compiler.get_importable_modules()) else { return Ok(None); };
Ok(this.definition_location_to_lsp_location(&line_numbers, ¶ms, location)) }) }
pub(crate) fn goto_type_definition( &mut self, params: lsp_types::TypeDefinitionParams, ) -> Response<Vec<lsp::Location>> { self.respond(|this| { let params = params.text_document_position_params; let (line_numbers, node) = match this.node_at_position(¶ms) { Some(location) => location, None => return Ok(vec![]), };
let Some(locations) = node .type_definition_locations(this.compiler.project_compiler.get_importable_modules()) else { return Ok(vec![]); };
let locations = locations .into_iter() .filter_map(|location| { this.definition_location_to_lsp_location(&line_numbers, ¶ms, location) }) .collect_vec();
Ok(locations) }) }
fn definition_location_to_lsp_location( &self, line_numbers: &LineNumbers, params: &lsp_types::TextDocumentPositionParams, location: DefinitionLocation, ) -> Option<lsp::Location> { let (uri, line_numbers) = match location.module { None => (params.text_document.uri.clone(), line_numbers), Some(name) => { let module = self.compiler.get_source(&name)?; let url = Url::parse(&format!("file:///{}", module.path)) .expect("goto definition URL parse"); (url, &module.line_numbers) } }; let range = src_span_to_lsp_range(location.span, line_numbers);
Some(lsp::Location { uri, range }) }
pub fn completion( &mut self, params: lsp::TextDocumentPositionParams, src: EcoString, ) -> Response<Option<Vec<lsp::CompletionItem>>> { self.respond(|this| { let module = match this.module_for_uri(¶ms.text_document.uri) { Some(module) => module, None => return Ok(None), };
let mut completer = Completer::new(&src, ¶ms, &this.compiler, module); let byte_index = completer.module_line_numbers.byte_index(params.position);
// If in comment context, do not provide completions if module.extra.is_within_comment(byte_index) { return Ok(None); }
// Check current file contents if the user is writing an import // and handle separately from the rest of the completion flow // Check if an import is being written if let Some(value) = completer.import_completions() { return value; }
let Some(found) = module.find_node(byte_index) else { return Ok(None); };
let completions = match found { Located::PatternSpread { .. } => None, Located::Pattern(_pattern) => None, Located::StringPrefixPatternVariable { .. } => None,
// Do not show completions when typing inside a string. Located::Expression { expression: TypedExpr::String { .. }, .. } | Located::Constant(Constant::String { .. }) => None,
Located::Expression { expression: TypedExpr::Call { fun, arguments, .. } | TypedExpr::RecordUpdate { constructor: fun, arguments, .. }, .. } => { let mut completions = vec![]; completions.append(&mut completer.completion_values()); completions.append(&mut completer.completion_labels(fun, arguments)); Some(completions) }
Located::Expression { expression: TypedExpr::RecordAccess { record, type_, .. }, .. } => { completer.expected_type = Some(type_.clone()); let mut completions = vec![]; completions.append(&mut completer.completion_values()); completions.append(&mut completer.completion_field_accessors(record.type_())); Some(completions) }
Located::Expression { position: ExpressionPosition::ArgumentOrLabel { called_function, function_arguments, }, .. } => { let mut completions = vec![]; completions.append(&mut completer.completion_values()); completions.append( &mut completer.completion_labels(called_function, function_arguments), ); Some(completions) }
// If we're typing inside an expression body (and not being any // more specific than this, meaning we're not editing some // specific value inside it) we don't want to set the expected // type to the type of the entire anonymous function, otherwise // the language server would start recommending the wrong // values: // // ``` // fn(x: Int) -> String { // | // <- Typing here // // We don't want the language server to suggest values // // of type `fn(Int) -> String`! // todo // } // ``` // Located::Expression { position: ExpressionPosition::Expression, expression: TypedExpr::Fn { .. }, } => Some(completer.completion_values()),
Located::Expression { expression, .. } => { completer.expected_type = Some(expression.type_()); Some(completer.completion_values()) }
Located::ModuleFunction(_) => Some(completer.completion_types()),
Located::Statement(_) => Some(completer.completion_values()),
Located::FunctionBody(_) => Some(completer.completion_values()),
Located::ModuleTypeAlias(_) | Located::ModuleCustomType(_) | Located::VariantConstructorDefinition(_) => Some(completer.completion_types()),
// If the import completions returned no results and we are in an import then // we should try to provide completions for unqualified values Located::ModuleImport(import) => this .compiler .get_module_interface(import.module.as_str()) .map(|importing_module| { completer.unqualified_completions_from_module(importing_module, true) }),
Located::ModuleConstant(_) | Located::Constant(_) => { Some(completer.completion_values()) }
Located::UnqualifiedImport(_) => None,
Located::Arg(_) => None,
Located::Annotation { .. } => Some(completer.completion_types()),
Located::Label { .. } | Located::RecordLabelDefinition { .. } | Located::RecordLabelUsage { .. } => None,
Located::RecordAccessLabel { field_type, record_type, .. } => { completer.expected_type = Some(field_type); let mut completions = vec![]; completions.append(&mut completer.completion_values()); completions.append(&mut completer.completion_field_accessors(record_type)); Some(completions) }
Located::ModuleName { layer: ast::Layer::Type, .. } => Some(completer.completion_types()), Located::ModuleName { layer: ast::Layer::Value, .. } => Some(completer.completion_values()),
Located::ClauseGuard(_) => Some(completer.completion_values()),
Located::TypeVariable { .. } => None, };
Ok(completions) }) }
pub fn code_actions( &mut self, params: lsp::CodeActionParams, ) -> Response<Option<Vec<CodeAction>>> { self.respond(|this| { let mut actions = vec![]; let Some(module) = this.module_for_uri(¶ms.text_document.uri) else { return Ok(None); };
let lines = LineNumbers::new(&module.code);
code_action_unused_values(module, &lines, ¶ms, &mut actions); actions.extend(RemoveUnusedImports::new(module, &lines, ¶ms).code_actions()); code_action_fix_names(module, &lines, ¶ms, &this.error, &mut actions); code_action_import_module(module, &lines, ¶ms, &this.error, &mut actions); code_action_generate_type(module, &lines, ¶ms, &this.error, &mut actions); code_action_add_missing_patterns(module, &lines, ¶ms, &this.error, &mut actions); actions .extend(RemoveUnreachableCaseClauses::new(module, &lines, ¶ms).code_actions()); actions .extend(RemoveRedundantRecordUpdate::new(module, &lines, ¶ms).code_actions()); actions.extend(CollapseNestedCase::new(module, &lines, ¶ms).code_actions()); actions.extend(FixBinaryOperation::new(module, &lines, ¶ms).code_actions()); actions .extend(FixTruncatedBitArraySegment::new(module, &lines, ¶ms).code_actions()); actions.extend(RemovePrivateOpaque::new(module, &lines, ¶ms).code_actions()); actions.extend(AddMissingTypeParameter::new(module, &lines, ¶ms).code_actions()); code_action_convert_qualified_constructor_to_unqualified( module, &this.compiler, &lines, ¶ms, &mut actions, ); code_action_convert_unqualified_constructor_to_qualified( module, &lines, ¶ms, &mut actions, ); code_action_inexhaustive_let_to_case( module, &lines, ¶ms, &this.error, &mut actions, ); actions.extend(MergeCaseBranches::new(module, &lines, ¶ms).code_actions()); actions.extend(LetAssertToCase::new(module, &lines, ¶ms).code_actions()); actions .extend(RedundantTupleInCaseSubject::new(module, &lines, ¶ms).code_actions()); actions.extend(FillInMissingLabelledArgs::new(module, &lines, ¶ms).code_actions()); actions.extend(UseLabelShorthandSyntax::new(module, &lines, ¶ms).code_actions()); actions.extend(ConvertFromUse::new(module, &lines, ¶ms).code_actions()); actions.extend(RemoveEchos::new(module, &lines, ¶ms).code_actions()); actions.extend(ConvertToUse::new(module, &lines, ¶ms).code_actions()); actions.extend(ExpandFunctionCapture::new(module, &lines, ¶ms).code_actions()); actions.extend(FillUnusedFields::new(module, &lines, ¶ms).code_actions()); actions.extend(InterpolateString::new(module, &lines, ¶ms).code_actions()); actions.extend(ExtractVariable::new(module, &lines, ¶ms).code_actions()); actions.extend(ExtractConstant::new(module, &lines, ¶ms).code_actions()); actions.extend( GenerateFunction::new(module, &this.compiler.modules, &lines, ¶ms) .code_actions(), ); actions.extend( GenerateVariant::new(module, &this.compiler, &lines, ¶ms).code_actions(), ); actions.extend(ConvertToPipe::new(module, &lines, ¶ms).code_actions()); actions.extend(ConvertToFunctionCall::new(module, &lines, ¶ms).code_actions()); actions.extend( PatternMatchOnValue::new(module, &lines, ¶ms, &this.compiler).code_actions(), ); actions.extend(AddOmittedLabels::new(module, &lines, ¶ms).code_actions()); actions.extend(InlineVariable::new(module, &lines, ¶ms).code_actions()); actions.extend(WrapInBlock::new(module, &lines, ¶ms).code_actions()); actions.extend(RemoveBlock::new(module, &lines, ¶ms).code_actions()); actions.extend(ExtractFunction::new(module, &lines, ¶ms).code_actions()); GenerateDynamicDecoder::new(module, &lines, ¶ms, &mut actions, &this.compiler) .code_actions(); actions.extend(WrapInAnonymousFunction::new(module, &lines, ¶ms).code_actions()); actions.extend(UnwrapAnonymousFunction::new(module, &lines, ¶ms).code_actions()); GenerateJsonEncoder::new( module, &lines, ¶ms, &mut actions, &this.compiler.project_compiler.config, ) .code_actions(); AddAnnotations::new(module, &lines, ¶ms).code_action(&mut actions); actions .extend(AnnotateTopLevelDefinitions::new(module, &lines, ¶ms).code_actions()); actions.extend(ReplaceUnderscoreWithType::new(module, &lines, ¶ms).code_actions()); actions.extend( CreateUnknownModule::new( module, &this.compiler, &lines, ¶ms, &this.paths, &this.error, ) .code_actions(), ); actions.extend(DiscardUnusedVariable::new(module, &lines, ¶ms).code_actions()); code_action_fix_deprecated_pipe(module, &lines, ¶ms, &mut actions); actions.extend( ConvertBetweenDocAndRegularComment::new(module, &lines, ¶ms).code_actions(), ); actions.extend(ConvertIntToDifferentBase::new(module, &lines, ¶ms).code_actions());
actions.sort_by_key(|one| { let preferred_key = if one.is_preferred == Some(true) { 0 } else { 1 }; let kind_key = match &one.kind { Some(lsp_types::CodeActionKind::QuickFix) => 1, Some(lsp_types::CodeActionKind::Refactor) => 2, Some(lsp_types::CodeActionKind::RefactorExtract) => 2, Some(lsp_types::CodeActionKind::RefactorInline) => 2, Some(lsp_types::CodeActionKind::RefactorMove) => 2, Some(lsp_types::CodeActionKind::RefactorRewrite) => 2, Some(lsp_types::CodeActionKind::Source) => 3, Some(lsp_types::CodeActionKind::SourceOrganizeImports) => 3, Some(lsp_types::CodeActionKind::SourceFixAll) => 3, Some(lsp_types::CodeActionKind::Custom(_)) => 4, Some(lsp_types::CodeActionKind::Notebook) => 5, Some(lsp_types::CodeActionKind::Empty) => 6, None => 7, }; (preferred_key, kind_key) });
Ok(if actions.is_empty() { None } else { Some(actions) }) }) }
pub fn document_symbol( &mut self, params: lsp::DocumentSymbolParams, ) -> Response<Vec<DocumentSymbol>> { self.respond(|this| { let mut symbols = vec![]; let Some(module) = this.module_for_uri(¶ms.text_document.uri) else { return Ok(symbols); }; let line_numbers = LineNumbers::new(&module.code);
for function in &module.ast.definitions.functions { // By default, the function's location ends right after the return type. // For the full symbol range, have it end at the end of the body. // Also include the documentation, if available. // // By convention, the symbol span starts from the leading slash in the // documentation comment's marker ('///'), not from its content (of which // we have the position), so we must convert the content start position // to the leading slash's position. let full_function_span = SrcSpan { start: function .documentation .as_ref() .map(|(doc_start, _)| get_doc_marker_position(*doc_start)) .unwrap_or(function.location.start),
end: function.end_position, };
let (name_location, name) = function .name .as_ref() .expect("Function in a definition must be named");
// The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] symbols.push(DocumentSymbol { name: name.to_string(), detail: Some( Printer::new(&module.ast.names) .print_type(&get_function_type(function)) .to_string(), ), kind: SymbolKind::Function, tags: make_deprecated_symbol_tag(&function.deprecation), deprecated: None, range: src_span_to_lsp_range(full_function_span, &line_numbers), selection_range: src_span_to_lsp_range(*name_location, &line_numbers), children: None, }); }
for alias in &module.ast.definitions.type_aliases { let full_alias_span = match alias.documentation { Some((doc_position, _)) => { SrcSpan::new(get_doc_marker_position(doc_position), alias.location.end) } None => alias.location, };
// The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] symbols.push(DocumentSymbol { name: alias.alias.to_string(), detail: Some( Printer::new(&module.ast.names) // If we print with aliases, we end up printing the alias which the user // is currently hovering, which is not helpful. Instead, we print the // raw type, so the user can see which type the alias represents .print_type_without_aliases(&alias.type_) .to_string(), ), kind: SymbolKind::Class, tags: make_deprecated_symbol_tag(&alias.deprecation), deprecated: None, range: src_span_to_lsp_range(full_alias_span, &line_numbers), selection_range: src_span_to_lsp_range(alias.name_location, &line_numbers), children: None, }); }
for custom_type in &module.ast.definitions.custom_types { symbols.push(custom_type_symbol(custom_type, &line_numbers, module)); }
for constant in &module.ast.definitions.constants { // `ModuleConstant.location` ends at the constant's name or type. // For the full symbol span, necessary for `range`, we need to // include the constant value as well. // Also include the documentation at the start, if available. let full_constant_span = SrcSpan { start: constant .documentation .as_ref() .map(|(doc_start, _)| get_doc_marker_position(*doc_start)) .unwrap_or(constant.location.start),
end: constant.value.location().end, };
// The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] symbols.push(DocumentSymbol { name: constant.name.to_string(), detail: Some( Printer::new(&module.ast.names) .print_type(&constant.type_) .to_string(), ), kind: SymbolKind::Constant, tags: make_deprecated_symbol_tag(&constant.deprecation), deprecated: None, range: src_span_to_lsp_range(full_constant_span, &line_numbers), selection_range: src_span_to_lsp_range(constant.name_location, &line_numbers), children: None, }); }
Ok(symbols) }) }
pub fn folding_range( &mut self, params: lsp::FoldingRangeParams, ) -> Response<Vec<FoldingRange>> { self.respond(|this| { let mut ranges: Vec<FoldingRange> = vec![]; let Some(module) = this.module_for_uri(¶ms.text_document.uri) else { return Ok(vec![]); };
let line_numbers = LineNumbers::new(&module.code);
for import in import_folding_spans(&module.ast.definitions.imports, &module.code, &line_numbers) { let Some(range) = folding_range_for_span(import, &line_numbers, Some(FoldingRangeKind::Imports)) else { continue; };
ranges.push(range); }
for type_ in &module.ast.definitions.custom_types { let span = type_.full_location(); let Some(range) = folding_range_for_span(span, &line_numbers, None) else { continue; }; ranges.push(range); }
for constant in &module.ast.definitions.constants { let span = SrcSpan::new(constant.location.start, constant.value.location().end); let Some(range) = folding_range_for_span(span, &line_numbers, None) else { continue; }; ranges.push(range); }
for alias in &module.ast.definitions.type_aliases { let span = alias.location; let Some(range) = folding_range_for_span(span, &line_numbers, None) else { continue; }; ranges.push(range); }
for function in &module.ast.definitions.functions { let Some(body_start) = function.body_start else { continue; };
let span = SrcSpan::new(body_start, function.end_position); let Some(range) = folding_range_for_span(span, &line_numbers, None) else { continue; }; ranges.push(range); }
for doc_comment in comment_folding_spans( &module.extra.doc_comments, &module.code, &line_numbers, "///", ) { let Some(range) = folding_range_for_span( doc_comment, &line_numbers, Some(FoldingRangeKind::Comment), ) else { continue; }; ranges.push(range); }
for module_comment in comment_folding_spans( &module.extra.module_comments, &module.code, &line_numbers, "////", ) { let Some(range) = folding_range_for_span( module_comment, &line_numbers, Some(FoldingRangeKind::Comment), ) else { continue; }; ranges.push(range); }
for comment in comment_folding_spans(&module.extra.comments, &module.code, &line_numbers, "//") { let Some(range) = folding_range_for_span(comment, &line_numbers, Some(FoldingRangeKind::Comment)) else { continue; }; ranges.push(range); }
ranges.sort_by_key(|range| range.start_line); Ok(ranges) }) }
/// Check whether a particular module is in the same package as this one fn is_same_package(&self, current_module: &Module, module_name: &str) -> bool { let other_module = self .compiler .project_compiler .get_importable_modules() .get(module_name); match other_module { // We can't rename values from other packages if we are not aliasing an unqualified import. Some(module) => module.package == current_module.ast.type_info.package, None => false, } }
pub fn prepare_rename( &mut self, params: lsp::PrepareRenameParams, ) -> Response<Option<PrepareRenameResult>> { self.respond(|this| { let (lines, found) = match this.node_at_position(¶ms.text_document_position_params) { Some(value) => value, None => return Ok(None), };
let Some(current_module) = this.module_for_uri(¶ms.text_document_position_params.text_document.uri) else { return Ok(None); };
let success_response = |location| { Some(PrepareRenameResult::Range(src_span_to_lsp_range( location, &lines, ))) };
let byte_index = lines.byte_index(params.text_document_position_params.position);
let referenced = reference_for_ast_node(found, ¤t_module.name);
Ok(match referenced { Some(Referenced::LocalVariable { location, origin, .. }) if location.contains(byte_index) => match origin.map(|origin| origin.syntax) { Some(VariableSyntax::Generated) => None, Some( VariableSyntax::Variable(label) | VariableSyntax::LabelShorthand(label), ) => success_response(SrcSpan { start: location.start, end: label .len() .try_into() .map(|len: u32| location.start + len) .unwrap_or(location.end), }), Some(VariableSyntax::AssignmentPattern(..)) | None => { success_response(location) } }, Some( Referenced::ModuleValue { module, location, name_start, target_kind, .. } | Referenced::ModuleType { module, location, name_start, target_kind, .. }, ) if location.contains(byte_index) => { // We can't rename types or values from other packages if we are not aliasing an unqualified import. let rename_allowed = match target_kind { RenameTarget::Qualified => this.is_same_package(current_module, &module), RenameTarget::Unqualified | RenameTarget::Definition => true, }; if rename_allowed { // The location field is sometimes larger than the // location of the actual name. In most cases they are // the same, but in import statements that are for types // and/or are aliased, the location is larger than the // name. // // For example, in `import m.{type Wibble as Wobble}`, // location will cover `type Wibble as Wobble` but // the name is just `Wobble`. In every case (including // non-imports), the name is at the very end of the // location span. let location = SrcSpan { start: name_start, end: location.end, }; success_response(location) } else { None } } Some(Referenced::ModuleName { location, module_alias, .. }) => success_response(SrcSpan::new( // Since the location contains the full module name (e.g. `wibble/wobble/woo`), // we just want to include the last segment so we get a rename of the string // `woo`, as that's the being referenced in module access expressions. location.end - module_alias.len() as u32, location.end, )),
Some(Referenced::TypeVariable { location, name: _ }) => success_response(location),
// For a label written using the shorthand syntax the location // spans the whole `label:`, so we trim it down to just the // label. Some(Referenced::Label { location, label, type_module, .. }) if this.is_same_package(current_module, &type_module) => { success_response(SrcSpan { start: location.start, end: location.start + label.len() as u32, }) }
_ => None, }) }) }
pub fn rename( &mut self, params: lsp::RenameParams, ) -> Response<Result<Option<WorkspaceEdit>, ResponseError>> { self.respond(|this| { let position = ¶ms.text_document_position_params;
let (lines, found) = match this.node_at_position(position) { Some(value) => value, None => return Ok(RenameOutcome::NoRenames.into_result()), };
let Some(module) = this.module_for_uri(&position.text_document.uri) else { return Ok(RenameOutcome::NoRenames.into_result()); };
let referenced = reference_for_ast_node(found, &module.name);
Ok(match referenced { Some(Referenced::LocalVariable { origin, definition_location, name, .. }) => { let rename_kind = match origin.map(|origin| origin.syntax) { Some(VariableSyntax::Generated) => { return Ok(RenameOutcome::NoRenames.into_result()); } Some(VariableSyntax::LabelShorthand(_)) => { VariableReferenceKind::LabelShorthand } Some( VariableSyntax::AssignmentPattern(..) | VariableSyntax::Variable { .. }, ) | None => VariableReferenceKind::Variable, }; rename_local_variable( module, &lines, ¶ms, definition_location, name, rename_kind, ) .into_result() } Some(Referenced::ModuleValue { module: module_name, target_kind, name, name_kind, .. }) => rename_module_entity( ¶ms, module, this.compiler.project_compiler.get_importable_modules(), &this.compiler.sources, Renamed { module_name: &module_name, name: &name, name_kind, target_kind, layer: ast::Layer::Value, }, ) .into_result(),
Some(Referenced::ModuleType { module: module_name, target_kind, name, .. }) => rename_module_entity( ¶ms, module, this.compiler.project_compiler.get_importable_modules(), &this.compiler.sources, Renamed { module_name: &module_name, name: &name, name_kind: Named::Type, target_kind, layer: ast::Layer::Type, }, ) .into_result(),
Some(Referenced::ModuleName { module_name, .. }) => { rename_module_alias(module, &lines, ¶ms, &module_name).into_result() }
Some(Referenced::TypeVariable { location, name }) => { rename_type_variable(module, &lines, ¶ms, location, name).into_result() }
Some(Referenced::Label { type_module, type_name, label, .. }) => rename_label( ¶ms, &type_module, &type_name, &label, this.compiler.project_compiler.get_importable_modules(), &this.compiler.sources, ) .into_result(),
None => RenameOutcome::NoRenames.into_result(), }) }) }
/// Shared core of find references and document highlight. fn find_references_in_scope( &mut self, position: &lsp_types::TextDocumentPositionParams, search_scope: FindReferencesSearchScope, ) -> Option<Vec<lsp::Location>> { let (lines, found) = self.node_at_position(position)?;
let uri = position.text_document.uri.clone();
let source_module = self.module_for_uri(&uri)?;
let byte_index = lines.byte_index(position.position);
let referenced = reference_for_ast_node(found, &source_module.name);
match referenced { Some(Referenced::LocalVariable { origin, definition_location, location, name, }) if location.contains(byte_index) => match origin.map(|origin| origin.syntax) { Some(VariableSyntax::Generated) => None, Some( VariableSyntax::LabelShorthand(_) | VariableSyntax::AssignmentPattern(..) | VariableSyntax::Variable { .. }, ) | None => { let variable_references = FindVariableReferences::new(definition_location, name) .find_in_module(&source_module.ast);
let mut reference_locations = Vec::with_capacity(variable_references.len() + 1); reference_locations.push(lsp::Location { uri: uri.clone(), range: src_span_to_lsp_range(definition_location, &lines), });
for reference in variable_references { reference_locations.push(lsp::Location { uri: uri.clone(), range: src_span_to_lsp_range(reference.location, &lines), }); }
Some(reference_locations) } }, Some(Referenced::ModuleValue { module, name, location, .. }) if location.contains(byte_index) => match search_scope { FindReferencesSearchScope::AllModules => Some(find_module_references( module, name, self.compiler.project_compiler.get_importable_modules(), &self.compiler.sources, ast::Layer::Value, )), FindReferencesSearchScope::CurrentModule => { let source_information = self.compiler.get_source(&source_module.name)?; let source_module = self.compiler.get_module_interface(&source_module.name)?; Some(find_module_references_in_module( module, name, source_module, source_information, ast::Layer::Value, )) } }, Some(Referenced::ModuleType { module, name, .. }) => match search_scope { FindReferencesSearchScope::AllModules => Some(find_module_references( module, name, self.compiler.project_compiler.get_importable_modules(), &self.compiler.sources, ast::Layer::Type, )), FindReferencesSearchScope::CurrentModule => { let source_information = self.compiler.get_source(&source_module.name)?; let source_module = self.compiler.get_module_interface(&source_module.name)?; Some(find_module_references_in_module( module, name, source_module, source_information, ast::Layer::Type, )) } }, Some(Referenced::Label { type_module, type_name, label, .. }) => match search_scope { FindReferencesSearchScope::AllModules => Some(find_label_references( type_module, type_name, label, self.compiler.project_compiler.get_importable_modules(), &self.compiler.sources, )), FindReferencesSearchScope::CurrentModule => { let source_information = self.compiler.get_source(&source_module.name)?; let source_module = self.compiler.get_module_interface(&source_module.name)?; Some(find_label_references_in_module( type_module, type_name, label, source_module, source_information, )) } }, _ => None, } }
pub fn find_references( &mut self, params: lsp::ReferenceParams, ) -> Response<Option<Vec<lsp::Location>>> { self.respond(|this| { let position = ¶ms.text_document_position_params; Ok(this.find_references_in_scope(position, FindReferencesSearchScope::AllModules)) }) }
pub fn document_highlight( &mut self, params: lsp::DocumentHighlightParams, ) -> Response<Option<Vec<lsp::DocumentHighlight>>> { self.respond(|this| { let position = ¶ms.text_document_position_params; Ok(this .find_references_in_scope(position, FindReferencesSearchScope::CurrentModule) .map(|references| { references .into_iter() .map(|loc| lsp::DocumentHighlight { range: loc.range, kind: None, }) .collect() })) }) }
/// Triggers after the renaming of one or more `.gleam` files, updating any /// imports to those modules. pub fn rename_files(&mut self, renames: Vec<(Url, Url)>) -> Response<Option<WorkspaceEdit>> { self.respond(|this| { let mut changes = HashMap::new();
for (old_uri, new_uri) in renames { let Some(old_name) = this.module_name_for_uri(&old_uri) else { continue; }; let Some(new_name) = this.module_name_for_uri(&new_uri) else { continue; }; rename_module_occurrences( old_name, new_name, this.compiler.project_compiler.get_importable_modules(), &this.compiler.sources, &mut changes, ); }
Ok(Some(WorkspaceEdit { changes: Some(changes), document_changes: None, change_annotations: None, })) }) }
fn respond<T>(&mut self, handler: impl FnOnce(&mut Self) -> Result<T>) -> Response<T> { let result = handler(self); let warnings = self.take_warnings(); // TODO: test. Ensure hover doesn't report as compiled let compilation = if self.compiled_since_last_feedback { let modules = std::mem::take(&mut self.modules_compiled_since_last_feedback); self.compiled_since_last_feedback = false; Compilation::Yes(modules) } else { Compilation::No }; Response { result, warnings, compilation, } }
pub fn hover(&mut self, params: lsp::HoverParams) -> Response<Option<Hover>> { self.respond(|this| { let params = params.text_document_position_params;
let (lines, found) = match this.node_at_position(¶ms) { Some(value) => value, None => return Ok(None), };
let Some(module) = this.module_for_uri(¶ms.text_document.uri) else { return Ok(None); };
Ok(match found { Located::Statement(_) => None, // TODO: hover for statement Located::ModuleFunction(function) => { Some(hover_for_function_head(function, lines, module)) } Located::ModuleConstant(constant) => { Some(hover_for_module_constant(constant, lines, module)) } Located::Constant(constant) => Some(hover_for_constant(constant, lines, module)), Located::ModuleImport(import) => { let Some(module) = this.compiler.get_module_interface(&import.module) else { return Ok(None); }; Some(hover_for_module( module, import.location, &lines, &this.hex_deps, )) } Located::ModuleCustomType(custom_type) => { Some(hover_for_custom_type(custom_type, lines)) } Located::ModuleTypeAlias(_) => None, Located::VariantConstructorDefinition(constructor) => { Some(hover_for_constructor(constructor, lines, module)) } Located::UnqualifiedImport( import @ UnqualifiedImport { name, module: module_name, location, name_position: _, as_name: _, }, ) => this .compiler .get_module_interface(module_name.as_str()) .and_then(|module_interface| { if import.is_type() { module_interface.types.get(name).map(|constructor| { hover_for_annotation( *location, constructor.type_.as_ref(), Some(constructor), lines, module, ) }) } else { module_interface.values.get(name).map(|v| { let m = if this.hex_deps.contains(&module_interface.package) { Some(module_interface) } else { None }; hover_for_imported_value(v, location, lines, m, name, module) }) } }), Located::Pattern(pattern) => Some(hover_for_pattern(pattern, lines, module)), Located::PatternSpread { spread_location, pattern, } => { let range = Some(src_span_to_lsp_range(spread_location, &lines));
let mut printer = Printer::new(&module.ast.names);
let PatternUnusedArguments { positional, labelled, } = pattern.unused_arguments().unwrap_or_default();
let positional = positional .iter() .map(|type_| format!("- `{}`", printer.print_type(type_))) .join("\n"); let labelled = labelled .iter() .map(|(label, type_)| { format!("- `{}: {}`", label, printer.print_type(type_)) }) .join("\n");
let content = match (positional.is_empty(), labelled.is_empty()) { (true, false) => format!("Unused labelled fields:\n{labelled}"), (false, true) => format!("Unused positional fields:\n{positional}"), (_, _) => format!( "Unused positional fields:{positional}
Unused labelled fields:{labelled}" ), };
Some(Hover { contents: Contents::MarkupContent(MarkupContent { value: content, kind: lsp_types::MarkupKind::Markdown, }), range, }) } Located::StringPrefixPatternVariable { location, .. } => Some( hover_for_string_prefix_pattern_variable(location, &lines, module), ), Located::Expression { expression, position, } => Some(hover_for_expression( expression, position, lines, module, &this.hex_deps, )), Located::Arg(arg) => Some(hover_for_function_argument(arg, lines, module)), Located::FunctionBody(_) => None, Located::Annotation { ast, type_ } => { let type_constructor = type_constructor_from_modules( this.compiler.project_compiler.get_importable_modules(), type_.clone(), ); Some(hover_for_annotation( ast.location(), &type_, type_constructor, lines, module, )) } Located::Label { location, field_type, } | Located::RecordLabelDefinition { location, field_type, .. } | Located::RecordLabelUsage { location, field_type, .. } => Some(hover_for_label(location, field_type, None, lines, module)), Located::RecordAccessLabel { location, field_type, documentation, .. } => Some(hover_for_label( location, field_type, documentation.as_ref(), lines, module, )), Located::ModuleName { location, module_name, .. } => { let Some(module) = this.compiler.get_module_interface(&module_name) else { return Ok(None); }; Some(hover_for_module(module, location, &lines, &this.hex_deps)) }
Located::ClauseGuard(guard) => Some(hover_for_clause_guard(guard, lines, module)),
Located::TypeVariable { .. } => None, }) }) }
pub(crate) fn signature_help( &mut self, params: lsp_types::SignatureHelpParams, ) -> Response<Option<SignatureHelp>> { self.respond(|this| { let Some(module) = this.module_for_uri(¶ms.text_document_position_params.text_document.uri) else { return Ok(None); };
match this.node_at_position(¶ms.text_document_position_params) { Some((_lines, Located::Expression { expression, .. })) => { Ok(signature_help::for_expression(expression, module)) } Some((_lines, _located)) => Ok(None), None => Ok(None), } }) }
fn module_node_at_position( &self, params: &lsp::TextDocumentPositionParams, module: &'a Module, ) -> Option<(LineNumbers, Located<'a>)> { let line_numbers = LineNumbers::new(&module.code); let byte_index = line_numbers.byte_index(params.position); let node = module.find_node(byte_index); let node = node?; Some((line_numbers, node)) }
fn node_at_position( &self, params: &lsp::TextDocumentPositionParams, ) -> Option<(LineNumbers, Located<'_>)> { let module = self.module_for_uri(¶ms.text_document.uri)?; self.module_node_at_position(params, module) }
fn module_name_for_uri(&self, uri: &Url) -> Option<EcoString> { // The to_file_path method is available on these platforms #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] let path = uri.to_file_path().expect("URL file");
#[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))] let path: Utf8PathBuf = uri.path().into();
let components = path .strip_prefix(self.paths.root()) .ok()? .components() .skip(1) .map(|c| c.as_os_str().to_string_lossy()); let name = Itertools::intersperse(components, "/".into()) .collect::<String>() .strip_suffix(".gleam")? .into(); Some(name) }
fn module_for_uri(&self, uri: &Url) -> Option<&Module> { let module_name = self.module_name_for_uri(uri)?; self.compiler.modules.get(&module_name) }
#[cfg(test)] pub fn path_for_module_name(&self, module_name: &str) -> Utf8PathBuf { let src_directory = self.paths.src_directory(); src_directory.join(module_name).with_extension("gleam") }}
fn import_folding_spans( imports: &[ast::Import<EcoString>], code: &str, line_numbers: &LineNumbers,) -> Vec<SrcSpan> { let mut spans = vec![]; let mut imports = imports.iter();
let Some(first_import) = imports.next() else { return spans; };
let mut previous_line = src_span_to_lsp_range( SrcSpan::new(first_import.location.start, first_import.location.start), line_numbers, ) .start .line; let mut current_start = first_import.location.start; let mut current_end = first_import.location.end; let mut current_len = 1;
for import in imports { let next_line = src_span_to_lsp_range( SrcSpan::new(import.location.start, import.location.start), line_numbers, ) .start .line; let separated_by_blank_line = next_line > previous_line + 1; let between = &code[current_end as usize..import.location.start as usize]; let has_non_whitespace_between = between.chars().any(|char| !char.is_whitespace());
if separated_by_blank_line || has_non_whitespace_between { if current_len > 1 { spans.push(SrcSpan::new(current_start, current_end)); } current_start = import.location.start; current_end = import.location.end; current_len = 1; } else { current_end = import.location.end; current_len += 1; }
previous_line = next_line; }
if current_len > 1 { spans.push(SrcSpan::new(current_start, current_end)); }
spans}
fn comment_folding_spans( comments: &[SrcSpan], code: &str, line_numbers: &LineNumbers, comment_prefix: &str,) -> Vec<SrcSpan> { let mut spans = vec![]; let comments = comments.iter();
let mut comments = comments.map(|span| { let line = src_span_to_lsp_range(*span, line_numbers).start.line; let start = line_numbers .line_starts .get(line as usize) .copied() .unwrap_or_default(); let end = line_numbers .line_starts .get(line as usize + 1) .copied() .map(|next_line_start| next_line_start.saturating_sub(1)) .unwrap_or(line_numbers.length);
// Find prefix (`//`, `///` or `////`), so we use start of the prefix // as start of comment. This allows us to not fold comments before and // after some code, like here: // ```gleam // // wibble // // wobble // wibble // wubble // ``` let between = &code[start as usize..end as usize]; let start = start + between.find(comment_prefix).unwrap_or_default() as u32; (start, line, end) });
let Some((start, line, end)) = comments.next() else { return spans; };
let mut current_start = start; let mut current_line = line; let mut current_end = end; let mut current_len = 1;
for (start, line, end) in comments { let separated_by_blank_line = line > current_line + 1; let between = &code[current_end as usize..start as usize]; let has_non_comments_between_between = between.chars().any(|char| !char.is_whitespace());
if separated_by_blank_line || has_non_comments_between_between { if current_len > 1 { spans.push(SrcSpan::new(current_start, current_end)); } current_start = start; current_end = end; current_len = 1; } else { current_end = end; current_len += 1; } current_line = line; }
if current_len > 1 { spans.push(SrcSpan::new(current_start, current_end)); }
spans}
fn folding_range_for_span( span: SrcSpan, line_numbers: &LineNumbers, kind: Option<FoldingRangeKind>,) -> Option<FoldingRange> { let range = src_span_to_lsp_range(span, line_numbers);
if range.start.line >= range.end.line { return None; }
Some(FoldingRange { start_line: range.start.line, start_character: None, end_line: range.end.line, end_character: None, kind, collapsed_text: None, })}
fn custom_type_symbol( type_: &CustomType<Arc<Type>>, line_numbers: &LineNumbers, module: &Module,) -> DocumentSymbol { let constructors = type_ .constructors .iter() .map(|constructor| { let mut arguments = vec![];
// List named arguments as field symbols. for argument in &constructor.arguments { let Some((label_location, label)) = &argument.label else { continue; };
let full_arg_span = match argument.doc { Some((doc_position, _)) => { SrcSpan::new(get_doc_marker_position(doc_position), argument.location.end) } None => argument.location, };
// The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] arguments.push(DocumentSymbol { name: label.to_string(), detail: Some( Printer::new(&module.ast.names) .print_type(&argument.type_) .to_string(), ), kind: SymbolKind::Field, tags: None, deprecated: None, range: src_span_to_lsp_range(full_arg_span, line_numbers), selection_range: src_span_to_lsp_range(*label_location, line_numbers), children: None, }); }
// Start from the documentation if available, otherwise from the constructor's name, // all the way to the end of its arguments. let full_constructor_span = SrcSpan { start: constructor .documentation .as_ref() .map(|(doc_start, _)| get_doc_marker_position(*doc_start)) .unwrap_or(constructor.location.start),
end: constructor.location.end, };
// The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] DocumentSymbol { name: constructor.name.to_string(), detail: None, kind: if constructor.arguments.is_empty() { SymbolKind::EnumMember } else { SymbolKind::Constructor }, tags: make_deprecated_symbol_tag(&constructor.deprecation), deprecated: None, range: src_span_to_lsp_range(full_constructor_span, line_numbers), selection_range: src_span_to_lsp_range(constructor.name_location, line_numbers), children: if arguments.is_empty() { None } else { Some(arguments) }, } }) .collect_vec();
// The type's location, by default, ranges from "(pub) type" to the end of its name. // We need it to range to the end of its constructors instead for the full symbol range. // We also include documentation, if available, by LSP convention. let full_type_span = SrcSpan { start: type_ .documentation .as_ref() .map(|(doc_start, _)| get_doc_marker_position(*doc_start)) .unwrap_or(type_.location.start),
end: type_.end_position, };
// The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] DocumentSymbol { name: type_.name.to_string(), detail: None, kind: SymbolKind::Class, tags: make_deprecated_symbol_tag(&type_.deprecation), deprecated: None, range: src_span_to_lsp_range(full_type_span, line_numbers), selection_range: src_span_to_lsp_range(type_.name_location, line_numbers), children: if constructors.is_empty() { None } else { Some(constructors) }, }}
fn hover_for_pattern(pattern: &TypedPattern, line_numbers: LineNumbers, module: &Module) -> Hover { let documentation = pattern.get_documentation().unwrap_or_default();
// Show the type of the hovered node to the user let type_ = Printer::new(&module.ast.names).print_type(pattern.type_().as_ref()); let contents = format!( "```gleam{type_}```{documentation}" ); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(pattern.location(), &line_numbers)), }}
fn get_function_type(fun: &TypedFunction) -> Type { Type::Fn { arguments: fun .arguments .iter() .map(|argument| argument.type_.clone()) .collect(), return_: fun.return_type.clone(), }}
fn hover_for_function_head( fun: &TypedFunction, line_numbers: LineNumbers, module: &Module,) -> Hover { let empty_str = EcoString::from(""); let documentation = fun .documentation .as_ref() .map(|(_, doc)| doc) .unwrap_or(&empty_str); let function_type = get_function_type(fun); let formatted_type = Printer::new(&module.ast.names).print_type(&function_type); let contents = format!( "```gleam{formatted_type}```{documentation}" ); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(fun.location, &line_numbers)), }}
fn hover_for_function_argument( argument: &TypedArg, line_numbers: LineNumbers, module: &Module,) -> Hover { let type_ = Printer::new(&module.ast.names).print_type(&argument.type_); let contents = format!("```gleam\n{type_}\n```"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(argument.location, &line_numbers)), }}
fn hover_for_annotation( location: SrcSpan, annotation_type: &Type, type_constructor: Option<&TypeConstructor>, line_numbers: LineNumbers, module: &Module,) -> Hover { let empty_str = EcoString::from(""); let documentation = type_constructor .and_then(|constructor| constructor.documentation.as_ref()) .unwrap_or(&empty_str); // If a user is hovering an annotation, it's not very useful to show the // local representation of that type, since that's probably what they see // in the source code anyway. So here, we print the raw type, // which is probably more helpful. let type_ = Printer::new(&module.ast.names).print_type_without_aliases(annotation_type); let contents = format!( "```gleam{type_}```{documentation}" ); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(location, &line_numbers)), }}
fn hover_for_label( location: SrcSpan, type_: Arc<Type>, documentation: Option<&EcoString>, line_numbers: LineNumbers, module: &Module,) -> Hover { let type_ = Printer::new(&module.ast.names).print_type(&type_); let contents = match documentation { Some(documentation) => format!("```gleam\n{type_}\n```\n{documentation}"), None => format!("```gleam\n{type_}\n```"), }; Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(location, &line_numbers)), }}
fn hover_for_module_constant( constant: &ModuleConstant<Arc<Type>>, line_numbers: LineNumbers, module: &Module,) -> Hover { let empty_str = EcoString::from(""); let type_ = Printer::new(&module.ast.names).print_type(&constant.type_); let documentation = constant .documentation .as_ref() .map(|(_, doc)| doc) .unwrap_or(&empty_str); let contents = format!("```gleam\n{type_}\n```\n{documentation}"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(constant.location, &line_numbers)), }}
fn hover_for_constant( constant: &TypedConstant, line_numbers: LineNumbers, module: &Module,) -> Hover { let type_ = Printer::new(&module.ast.names).print_type(&constant.type_()); let contents = format!("```gleam\n{type_}\n```"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(constant.location(), &line_numbers)), }}
fn hover_for_clause_guard( guard: &TypedClauseGuard, line_numbers: LineNumbers, module: &Module,) -> Hover { let type_ = Printer::new(&module.ast.names).print_type(&guard.type_()); let contents = format!("```gleam\n{type_}\n```"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(guard.location(), &line_numbers)), }}
fn hover_for_string_prefix_pattern_variable( location: SrcSpan, lines: &LineNumbers, module: &Module,) -> Hover { let type_ = Printer::new(&module.ast.names).print_type(&type_::string()); let contents = format!("```gleam\n{type_}\n```"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(location, lines)), }}
fn hover_for_expression<'a>( expression: &'a TypedExpr, position: ExpressionPosition<'a>, line_numbers: LineNumbers, module: &Module, hex_deps: &HashSet<EcoString>,) -> Hover { let documentation = expression.get_documentation().unwrap_or_default();
let link_section = get_expr_qualified_name(expression) .and_then(|(module_name, name)| { get_hexdocs_link_section(module_name, name, &module.ast, hex_deps) }) .unwrap_or("".to_string());
// Show the type of the hovered node to the user let mut printer = Printer::new(&module.ast.names); let type_ = printer.print_type(expression.type_().as_ref());
// If the expression is a record update and there's record fields that are // begin implicitly updated, then we want to list them in the hover. let unchanged_record_update_fields = match position { ExpressionPosition::Expression | ExpressionPosition::ArgumentOrLabel { .. } => "",
ExpressionPosition::UpdatedRecord { unchanged_record_fields, } if !unchanged_record_fields.is_empty() => &format!( "Unchanged record fields:\n{}", unchanged_record_fields .iter() .map(|(label, type_)| format!("- `{}: {}`", label, printer.print_type(type_))) .join("\n") ), ExpressionPosition::UpdatedRecord { .. } => "", };
let description = [documentation, unchanged_record_update_fields, &link_section] .iter() .filter(|string| !string.is_empty()) .join("\n\n");
let contents = format!( "```gleam{type_}```{description}" );
Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(expression.location(), &line_numbers)), }}
fn hover_for_imported_value( value: &ValueConstructor, location: &SrcSpan, line_numbers: LineNumbers, hex_module_imported_from: Option<&ModuleInterface>, name: &EcoString, module: &Module,) -> Hover { let documentation = value.get_documentation().unwrap_or_default();
let link_section = hex_module_imported_from.map_or("".to_string(), |m| { format_hexdocs_link_section(m.package.as_str(), m.name.as_str(), Some(name)) });
// Show the type of the hovered node to the user let type_ = Printer::new(&module.ast.names).print_type(value.type_.as_ref()); let contents = format!( "```gleam{type_}```{documentation}{link_section}" ); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(*location, &line_numbers)), }}
fn hover_for_module( module: &ModuleInterface, location: SrcSpan, line_numbers: &LineNumbers, hex_deps: &HashSet<EcoString>,) -> Hover { let documentation = module.documentation.join("\n"); let name = &module.name;
let link_section = if hex_deps.contains(&module.package) { format_hexdocs_link_section(&module.package, name, None) } else { String::new() };
let contents = format!( "```gleam{name}```{documentation}{link_section}", ); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(location, line_numbers)), }}
fn hover_for_custom_type(type_: &CustomType<Arc<Type>>, line_numbers: LineNumbers) -> Hover { let name = &type_.name; let documentation = type_ .documentation .as_ref() .map(|(_, documentation)| documentation.clone()) .unwrap_or_default();
let contents = format!("```gleam\n{name}\n```\n{documentation}"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(type_.full_location(), &line_numbers)), }}
fn hover_for_constructor( constructor: &TypedRecordConstructor, line_numbers: LineNumbers, module: &Module,) -> Hover { let mut printer = Printer::new(&module.ast.names);
let arguments = constructor .arguments .iter() .map(|argument| match &argument.label { Some((_, label)) => eco_format!("{label}: {}", printer.print_type(&argument.type_)), None => printer.print_type(&argument.type_), }) .join(", ");
let documentation = constructor .documentation .as_ref() .map(|(_, documentation)| documentation.clone()) .unwrap_or_default();
let constructor_doc = if arguments.is_empty() { constructor.name.clone() } else { eco_format!("{}({arguments})", constructor.name) };
let contents = format!("```gleam\n{constructor_doc}\n```\n{documentation}"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(constructor.location, &line_numbers)), }}
/// Returns true if any part of either range overlaps with the other.pub fn overlaps(a: Range, b: Range) -> bool { position_within(a.start, b) || position_within(a.end, b) || position_within(b.start, a) || position_within(b.end, a)}
/// Returns true if the first range is within the second range./// The ranges might touch on their extremes and still be considered one within/// the other!////// `within(a, b)` is true in all of these cases:////// - ```txt/// |------| b/// |---| a/// ```/// - ```txt/// |------| b/// |---| a/// ```/// - ```txt/// |------| b/// |---| a/// ```/// - ```txt/// |------| b/// |------| a/// ```///pub fn within(a: Range, b: Range) -> bool { position_within(a.start, b) && position_within(a.end, b)}
/// Returns true if the first range is completely within the second range./// The range cannot have any extreme in common to be considered completely/// within another.////// `completely_within(a, b)` is true this case:////// - ```txt/// |------| b/// |---| a/// ```////// And `completely_within(a, b)` is false in all these cases:////// - ```txt/// |------| b/// |---| a/// ```/// - ```txt/// |------| b/// |---| a/// ```/// - ```txt/// |------| b/// |------| a/// ```///pub fn completely_within(a: Range, b: Range) -> bool { a.start > b.start && a.start < b.end && a.end > b.start && a.end < b.end}
// Returns true if a position is within a range.pub fn position_within(position: Position, range: Range) -> bool { position >= range.start && position <= range.end}
/// Builds the code action to assign an unused value to `_`.///fn code_action_unused_values( module: &Module, line_numbers: &LineNumbers, params: &lsp::CodeActionParams, actions: &mut Vec<CodeAction>,) { let uri = ¶ms.text_document.uri; let mut unused_values: Vec<&SrcSpan> = module .ast .type_info .warnings .iter() .filter_map(|warning| { if let type_::Warning::ImplicitlyDiscardedResult { location } = warning { Some(location) } else { None } }) .collect();
if unused_values.is_empty() { return; }
// Sort spans by start position, with longer spans coming first unused_values.sort_by_key(|span| (span.start, -(span.len() as i64)));
let mut processed_lsp_range = Vec::new();
for unused in unused_values { let SrcSpan { start, end } = *unused; let hover_range = src_span_to_lsp_range(SrcSpan::new(start, end), line_numbers);
// Check if this span is contained within any previously processed span if processed_lsp_range .iter() .any(|&prev_lsp_range| within(hover_range, prev_lsp_range)) { continue; }
// Check if the cursor is within this span if !within(params.range, hover_range) { continue; }
let edit = TextEdit { range: src_span_to_lsp_range(SrcSpan::new(start, start), line_numbers), new_text: "let _ = ".into(), };
CodeActionBuilder::new("Assign unused Result value to `_`") .kind(lsp_types::CodeActionKind::QuickFix) .changes(uri.clone(), vec![edit]) .preferred(true) .push_to(actions);
processed_lsp_range.push(hover_range); }}
struct NameCorrection { pub location: SrcSpan, pub correction: EcoString,}
fn code_action_fix_names( module: &Module, line_numbers: &LineNumbers, params: &lsp::CodeActionParams, error: &Option<Error>, actions: &mut Vec<CodeAction>,) { let uri = ¶ms.text_document.uri; let Some(errors) = type_errors_for_module(error, module) else { return; }; let name_corrections = errors .iter() .filter_map(|error| { if let type_::Error::BadName { location, name, kind, } = error { Some(NameCorrection { correction: correct_name_case(name, *kind), location: *location, }) } else { None } }) .collect_vec();
if name_corrections.is_empty() { return; }
for name_correction in name_corrections { let NameCorrection { location, correction, } = name_correction;
let range = src_span_to_lsp_range(location, line_numbers); // Check if the user's cursor is on the invalid name if overlaps(params.range, range) { let edit = TextEdit { range, new_text: correction.to_string(), };
CodeActionBuilder::new(&format!("Rename to {correction}")) .kind(lsp_types::CodeActionKind::QuickFix) .changes(uri.clone(), vec![edit]) .preferred(true) .push_to(actions); } }}
fn get_expr_qualified_name(expression: &TypedExpr) -> Option<(&EcoString, &EcoString)> { match expression { TypedExpr::Var { name, constructor, .. } if constructor.publicity.is_importable() => match &constructor.variant { ValueConstructorVariant::ModuleFn { module: module_name, .. } => Some((module_name, name)),
ValueConstructorVariant::ModuleConstant { module: module_name, .. } => Some((module_name, name)),
ValueConstructorVariant::LocalVariable { .. } | ValueConstructorVariant::Record { .. } => None, },
TypedExpr::ModuleSelect { label, module_name, .. } => Some((module_name, label)),
TypedExpr::Int { .. } | TypedExpr::Float { .. } | TypedExpr::String { .. } | TypedExpr::Block { .. } | TypedExpr::Pipeline { .. } | TypedExpr::Var { .. } | TypedExpr::Fn { .. } | TypedExpr::List { .. } | TypedExpr::Call { .. } | TypedExpr::BinOp { .. } | TypedExpr::Case { .. } | TypedExpr::RecordAccess { .. } | TypedExpr::PositionalAccess { .. } | TypedExpr::Tuple { .. } | TypedExpr::TupleIndex { .. } | TypedExpr::Todo { .. } | TypedExpr::Panic { .. } | TypedExpr::Echo { .. } | TypedExpr::BitArray { .. } | TypedExpr::RecordUpdate { .. } | TypedExpr::NegateBool { .. } | TypedExpr::NegateInt { .. } | TypedExpr::Invalid { .. } => None, }}
fn format_hexdocs_link_section( package_name: &str, module_name: &str, name: Option<&str>,) -> String { let package_name = package_name.replace('_', "-"); let link = match name { Some(name) => format!("https://{package_name}.hexdocs.pm/{module_name}.html#{name}"), None => format!("https://{package_name}.hexdocs.pm/{module_name}.html"), }; format!("\nView on [HexDocs]({link})")}
fn get_hexdocs_link_section( module_name: &str, name: &str, ast: &TypedModule, hex_deps: &HashSet<EcoString>,) -> Option<String> { let package_name = ast.definitions.imports.iter().find_map(|import| { if import.module == module_name && hex_deps.contains(&import.package) { Some(&import.package) } else { None } })?;
Some(format_hexdocs_link_section( package_name, module_name, Some(name), ))}
/// Converts the source start position of a documentation comment's contents into/// the position of the leading slash in its marker ('///').fn get_doc_marker_position(content_pos: u32) -> u32 { content_pos.saturating_sub(3)}
fn make_deprecated_symbol_tag(deprecation: &Deprecation) -> Option<Vec<SymbolTag>> { deprecation .is_deprecated() .then(|| vec![SymbolTag::Deprecated])}
enum FindReferencesSearchScope { AllModules, CurrentModule,}