Nebiroth updated this revision to Diff 125619. Nebiroth added a comment. Added static_cast when unparsing
Repository: rCTE Clang Tools Extra https://reviews.llvm.org/D38425 Files: clangd/ClangdLSPServer.cpp clangd/ClangdLSPServer.h clangd/ClangdServer.cpp clangd/ClangdServer.h clangd/ClangdUnit.cpp clangd/ClangdUnit.h clangd/Protocol.cpp clangd/Protocol.h clangd/ProtocolHandlers.cpp clangd/ProtocolHandlers.h test/clangd/documenthighlight.test test/clangd/initialize-params-invalid.test test/clangd/initialize-params.test
Index: test/clangd/initialize-params.test =================================================================== --- test/clangd/initialize-params.test +++ test/clangd/initialize-params.test @@ -20,6 +20,7 @@ # CHECK-NEXT: }, # CHECK-NEXT: "definitionProvider": true, # CHECK-NEXT: "documentFormattingProvider": true, +# CHECK-NEXT: "documentHighlightProvider": true, # CHECK-NEXT: "documentOnTypeFormattingProvider": { # CHECK-NEXT: "firstTriggerCharacter": "}", # CHECK-NEXT: "moreTriggerCharacter": [] Index: test/clangd/initialize-params-invalid.test =================================================================== --- test/clangd/initialize-params-invalid.test +++ test/clangd/initialize-params-invalid.test @@ -20,6 +20,7 @@ # CHECK-NEXT: }, # CHECK-NEXT: "definitionProvider": true, # CHECK-NEXT: "documentFormattingProvider": true, +# CHECK-NEXT: "documentHighlightProvider": true, # CHECK-NEXT: "documentOnTypeFormattingProvider": { # CHECK-NEXT: "firstTriggerCharacter": "}", # CHECK-NEXT: "moreTriggerCharacter": [] Index: test/clangd/documenthighlight.test =================================================================== --- /dev/null +++ test/clangd/documenthighlight.test @@ -0,0 +1,42 @@ +# RUN: clangd -run-synchronously < %s | FileCheck %s +# It is absolutely vital that this file has CRLF line endings. +# +Content-Length: 125 + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} + +Content-Length: 479 + +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///main.cpp","languageId":"cpp","version":1,"text":"#define MACRO 1\nnamespace ns1 {\nstruct MyClass {\nint xasd;\nvoid anotherOperation() {\n}\nstatic int foo(MyClass*) {\nreturn 0;\n}\n\n};\nstruct Foo {\nint xasd;\n};\n}\nint main() {\nint bonjour;\nbonjour = 2;\nint test1 = bonjour;\nns1::Foo bar = { xasd : 1};\nbar.xasd = 3;\nns1::MyClass* Params;\nParams->anotherOperation();\n}\n"}}} + +Content-Length: 156 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/documentHighlight","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":17,"character":2}}} +# Verify local variable +# CHECK: {"id":1,"jsonrpc":"2.0","result":[{"kind":1,"range":{"end":{"character":11,"line":16},"start":{"character":4,"line":16}}},{"kind":3,"range":{"end":{"character":7,"line":17},"start":{"character":0,"line":17}}},{"kind":2,"range":{"end":{"character":19,"line":18},"start":{"character":12,"line":18}}}]} + +Content-Length: 157 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/documentHighlight","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":18,"character":17}}} +# Verify struct highlight +# CHECK: {"id":1,"jsonrpc":"2.0","result":[{"kind":1,"range":{"end":{"character":11,"line":16},"start":{"character":4,"line":16}}},{"kind":3,"range":{"end":{"character":7,"line":17},"start":{"character":0,"line":17}}},{"kind":2,"range":{"end":{"character":19,"line":18},"start":{"character":12,"line":18}}}]} + +Content-Length: 157 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/documentHighlight","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":21,"character":10}}} +# Verify method highlight +# CHECK: {"id":1,"jsonrpc":"2.0","result":[{"kind":1,"range":{"end":{"character":14,"line":2},"start":{"character":7,"line":2}}},{"kind":1,"range":{"end":{"character":22,"line":6},"start":{"character":15,"line":6}}},{"kind":1,"range":{"end":{"character":12,"line":21},"start":{"character":5,"line":21}}}]} + +Content-Length: 157 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/documentHighlight","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":18,"character":14}}} +# Verify Read-access of a symbol (kind = 2) +# CHECK: {"id":1,"jsonrpc":"2.0","result":[{"kind":1,"range":{"end":{"character":11,"line":16},"start":{"character":4,"line":16}}},{"kind":3,"range":{"end":{"character":7,"line":17},"start":{"character":0,"line":17}}},{"kind":2,"range":{"end":{"character":19,"line":18},"start":{"character":12,"line":18}}}]} + +Content-Length: 48 + +{"jsonrpc":"2.0","id":10000,"method":"shutdown"} + +Content-Length: 33 + +{"jsonrpc":"2.0":"method":"exit"} \ No newline at end of file Index: clangd/ProtocolHandlers.h =================================================================== --- clangd/ProtocolHandlers.h +++ clangd/ProtocolHandlers.h @@ -54,6 +54,8 @@ virtual void onFileEvent(Ctx C, DidChangeWatchedFilesParams &Params) = 0; virtual void onCommand(Ctx C, ExecuteCommandParams &Params) = 0; virtual void onRename(Ctx C, RenameParams &Parames) = 0; + virtual void onDocumentHighlight(Ctx C, + TextDocumentPositionParams &Params) = 0; }; void registerCallbackHandlers(JSONRPCDispatcher &Dispatcher, JSONOutput &Out, Index: clangd/ProtocolHandlers.cpp =================================================================== --- clangd/ProtocolHandlers.cpp +++ clangd/ProtocolHandlers.cpp @@ -72,4 +72,6 @@ Register("textDocument/rename", &ProtocolCallbacks::onRename); Register("workspace/didChangeWatchedFiles", &ProtocolCallbacks::onFileEvent); Register("workspace/executeCommand", &ProtocolCallbacks::onCommand); + Register("textDocument/documentHighlight", + &ProtocolCallbacks::onDocumentHighlight); } Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -557,6 +557,38 @@ }; bool fromJSON(const json::Expr &, RenameParams &); +enum class DocumentHighlightKind { Text = 1, Read = 2, Write = 3 }; + +/// +/// A document highlight is a range inside a text document which deserves +/// special attention. Usually a document highlight is visualized by changing +/// the background color of its range. +/// +struct DocumentHighlight { + + /// + /// The range this highlight applies to. + /// + Range range; + + /// + /// The highlight kind, default is DocumentHighlightKind.Text. + /// + DocumentHighlightKind kind = DocumentHighlightKind::Text; + + friend bool operator<(const DocumentHighlight &LHS, + const DocumentHighlight &RHS) { + return std::tie(LHS.range) < std::tie(RHS.range) && + std::tie(LHS.kind) < std::tie(RHS.kind); + } + + friend bool operator==(const DocumentHighlight &LHS, + const DocumentHighlight &RHS) { + return LHS.kind == RHS.kind && LHS.range == RHS.range; + } +}; +json::Expr toJSON(const DocumentHighlight &DH); + } // namespace clangd } // namespace clang Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -359,5 +359,12 @@ O.map("position", R.position) && O.map("newName", R.newName); } +json::Expr toJSON(const DocumentHighlight &DH) { + return json::obj{ + {"range", toJSON(DH.range)}, + {"kind", static_cast<int>(DH.kind)}, + }; +} + } // namespace clangd } // namespace clang Index: clangd/ClangdUnit.h =================================================================== --- clangd/ClangdUnit.h +++ clangd/ClangdUnit.h @@ -256,15 +256,17 @@ clangd::Logger &Logger; }; - /// Get the beginning SourceLocation at a specified \p Pos. SourceLocation getBeginningOfIdentifier(ParsedAST &Unit, const Position &Pos, const FileEntry *FE); /// Get definition of symbol at a specified \p Pos. std::vector<Location> findDefinitions(ParsedAST &AST, Position Pos, clangd::Logger &Logger); +std::vector<DocumentHighlight> +findDocumentHighlights(ParsedAST &AST, Position Pos, clangd::Logger &Logger); + /// For testing/debugging purposes. Note that this method deserializes all /// unserialized Decls, so use with care. void dumpAST(ParsedAST &AST, llvm::raw_ostream &OS); Index: clangd/ClangdUnit.cpp =================================================================== --- clangd/ClangdUnit.cpp +++ clangd/ClangdUnit.cpp @@ -222,36 +222,44 @@ } /// Finds declarations locations that a given source location refers to. -class DeclarationLocationsFinder : public index::IndexDataConsumer { - std::vector<Location> DeclarationLocations; +class DeclarationAndMacrosFinder : public index::IndexDataConsumer { + std::vector<const Decl *> Decls; + std::vector<const MacroInfo *> MacroInfos; const SourceLocation &SearchedLocation; const ASTContext &AST; Preprocessor &PP; public: - DeclarationLocationsFinder(raw_ostream &OS, + DeclarationAndMacrosFinder(raw_ostream &OS, const SourceLocation &SearchedLocation, ASTContext &AST, Preprocessor &PP) : SearchedLocation(SearchedLocation), AST(AST), PP(PP) {} - std::vector<Location> takeLocations() { - // Don't keep the same location multiple times. + std::vector<const Decl *> takeDecls() { + // Don't keep the same declaration multiple times. // This can happen when nodes in the AST are visited twice. - std::sort(DeclarationLocations.begin(), DeclarationLocations.end()); - auto last = - std::unique(DeclarationLocations.begin(), DeclarationLocations.end()); - DeclarationLocations.erase(last, DeclarationLocations.end()); - return std::move(DeclarationLocations); + std::sort(Decls.begin(), Decls.end()); + auto Last = std::unique(Decls.begin(), Decls.end()); + Decls.erase(Last, Decls.end()); + return std::move(Decls); + } + + std::vector<const MacroInfo *> takeMacroInfos() { + // Don't keep the same Macro info multiple times. + // This can happen when nodes in the AST are visited twice. + std::sort(MacroInfos.begin(), MacroInfos.end()); + auto Last = std::unique(MacroInfos.begin(), MacroInfos.end()); + MacroInfos.erase(Last, MacroInfos.end()); + return std::move(MacroInfos); } bool handleDeclOccurence(const Decl *D, index::SymbolRoleSet Roles, ArrayRef<index::SymbolRelation> Relations, FileID FID, unsigned Offset, index::IndexDataConsumer::ASTNodeInfo ASTNode) override { - if (isSearchedLocation(FID, Offset)) { - addDeclarationLocation(D->getSourceRange()); - } + if (isSearchedLocation(FID, Offset)) + Decls.push_back(D); return true; } @@ -262,31 +270,6 @@ SourceMgr.getFileID(SearchedLocation) == FID; } - void addDeclarationLocation(const SourceRange &ValSourceRange) { - const SourceManager &SourceMgr = AST.getSourceManager(); - const LangOptions &LangOpts = AST.getLangOpts(); - SourceLocation LocStart = ValSourceRange.getBegin(); - SourceLocation LocEnd = Lexer::getLocForEndOfToken(ValSourceRange.getEnd(), - 0, SourceMgr, LangOpts); - Position Begin; - Begin.line = SourceMgr.getSpellingLineNumber(LocStart) - 1; - Begin.character = SourceMgr.getSpellingColumnNumber(LocStart) - 1; - Position End; - End.line = SourceMgr.getSpellingLineNumber(LocEnd) - 1; - End.character = SourceMgr.getSpellingColumnNumber(LocEnd) - 1; - Range R = {Begin, End}; - Location L; - if (const FileEntry *F = - SourceMgr.getFileEntryForID(SourceMgr.getFileID(LocStart))) { - StringRef FilePath = F->tryGetRealPathName(); - if (FilePath.empty()) - FilePath = F->getName(); - L.uri = URI::fromFile(FilePath); - L.range = R; - DeclarationLocations.push_back(L); - } - } - void finish() override { // Also handle possible macro at the searched location. Token Result; @@ -309,16 +292,111 @@ PP.getMacroDefinitionAtLoc(IdentifierInfo, BeforeSearchedLocation); MacroInfo *MacroInf = MacroDef.getMacroInfo(); if (MacroInf) { - addDeclarationLocation(SourceRange(MacroInf->getDefinitionLoc(), - MacroInf->getDefinitionEndLoc())); + MacroInfos.push_back(MacroInf); } } } } }; +/// Finds document highlights that a given list of declarations refers to. +class DocumentHighlightsFinder : public index::IndexDataConsumer { + std::vector<const Decl *> &Decls; + std::vector<DocumentHighlight> DocumentHighlights; + const ASTContext &AST; + +public: + DocumentHighlightsFinder(raw_ostream &OS, ASTContext &AST, Preprocessor &PP, + std::vector<const Decl *> &Decls) + : Decls(Decls), AST(AST) {} + std::vector<DocumentHighlight> takeHighlights() { + // Don't keep the same highlight multiple times. + // This can happen when nodes in the AST are visited twice. + std::sort(DocumentHighlights.begin(), DocumentHighlights.end()); + auto Last = + std::unique(DocumentHighlights.begin(), DocumentHighlights.end()); + DocumentHighlights.erase(Last, DocumentHighlights.end()); + return std::move(DocumentHighlights); + } + + bool + handleDeclOccurence(const Decl *D, index::SymbolRoleSet Roles, + ArrayRef<index::SymbolRelation> Relations, FileID FID, + unsigned Offset, + index::IndexDataConsumer::ASTNodeInfo ASTNode) override { + const SourceManager &SourceMgr = AST.getSourceManager(); + if (SourceMgr.getMainFileID() != FID || + std::find(Decls.begin(), Decls.end(), D) == Decls.end()) { + return true; + } + SourceLocation Begin, End; + const LangOptions &LangOpts = AST.getLangOpts(); + SourceLocation StartOfFileLoc = SourceMgr.getLocForStartOfFile(FID); + SourceLocation HightlightStartLoc = StartOfFileLoc.getLocWithOffset(Offset); + End = + Lexer::getLocForEndOfToken(HightlightStartLoc, 0, SourceMgr, LangOpts); + SourceRange SR(HightlightStartLoc, End); + + DocumentHighlightKind Kind = DocumentHighlightKind::Text; + if (static_cast<index::SymbolRoleSet>(index::SymbolRole::Write) & Roles) + Kind = DocumentHighlightKind::Write; + else if (static_cast<index::SymbolRoleSet>(index::SymbolRole::Read) & Roles) + Kind = DocumentHighlightKind::Read; + + DocumentHighlights.push_back(getDocumentHighlight(SR, Kind)); + return true; + } + +private: + DocumentHighlight getDocumentHighlight(SourceRange SR, + DocumentHighlightKind Kind) { + const SourceManager &SourceMgr = AST.getSourceManager(); + SourceLocation LocStart = SR.getBegin(); + Position Begin; + Begin.line = SourceMgr.getSpellingLineNumber(LocStart) - 1; + Begin.character = SourceMgr.getSpellingColumnNumber(LocStart) - 1; + Position End; + End.line = SourceMgr.getSpellingLineNumber(SR.getEnd()) - 1; + End.character = SourceMgr.getSpellingColumnNumber(SR.getEnd()) - 1; + Range R = {Begin, End}; + DocumentHighlight DH; + DH.range = R; + DH.kind = Kind; + return DH; + } +}; + } // namespace +llvm::Optional<Location> +getDeclarationLocation(ParsedAST &AST, const SourceRange &ValSourceRange) { + const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); + const LangOptions &LangOpts = AST.getASTContext().getLangOpts(); + SourceLocation LocStart = ValSourceRange.getBegin(); + + const FileEntry *F = + SourceMgr.getFileEntryForID(SourceMgr.getFileID(LocStart)); + if (!F) + return llvm::None; + SourceLocation LocEnd = Lexer::getLocForEndOfToken(ValSourceRange.getEnd(), 0, + SourceMgr, LangOpts); + Position Begin; + Begin.line = SourceMgr.getSpellingLineNumber(LocStart) - 1; + Begin.character = SourceMgr.getSpellingColumnNumber(LocStart) - 1; + Position End; + End.line = SourceMgr.getSpellingLineNumber(LocEnd) - 1; + End.character = SourceMgr.getSpellingColumnNumber(LocEnd) - 1; + Range R = {Begin, End}; + Location L; + + StringRef FilePath = F->tryGetRealPathName(); + if (FilePath.empty()) + FilePath = F->getName(); + L.uri = URI::fromFile(FilePath); + L.range = R; + return L; +} + std::vector<Location> clangd::findDefinitions(ParsedAST &AST, Position Pos, clangd::Logger &Logger) { const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); @@ -328,18 +406,69 @@ SourceLocation SourceLocationBeg = getBeginningOfIdentifier(AST, Pos, FE); - auto DeclLocationsFinder = std::make_shared<DeclarationLocationsFinder>( + auto DeclMacrosFinder = std::make_shared<DeclarationAndMacrosFinder>( + llvm::errs(), SourceLocationBeg, AST.getASTContext(), + AST.getPreprocessor()); + index::IndexingOptions IndexOpts; + IndexOpts.SystemSymbolFilter = + index::IndexingOptions::SystemSymbolFilterKind::All; + IndexOpts.IndexFunctionLocals = true; + + indexTopLevelDecls(AST.getASTContext(), AST.getTopLevelDecls(), + DeclMacrosFinder, IndexOpts); + + std::vector<const Decl *> Decls = DeclMacrosFinder->takeDecls(); + std::vector<const MacroInfo *> MacroInfos = + DeclMacrosFinder->takeMacroInfos(); + std::vector<Location> Result; + + for (auto Item : Decls) { + auto L = getDeclarationLocation(AST, Item->getSourceRange()); + if (L) + Result.push_back(*L); + } + + for (auto Item : MacroInfos) { + SourceRange SR(Item->getDefinitionLoc(), Item->getDefinitionEndLoc()); + auto L = getDeclarationLocation(AST, SR); + if (L) + Result.push_back(*L); + } + + return Result; +} + +std::vector<DocumentHighlight> +clangd::findDocumentHighlights(ParsedAST &AST, Position Pos, + clangd::Logger &Logger) { + const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); + const FileEntry *FE = SourceMgr.getFileEntryForID(SourceMgr.getMainFileID()); + if (!FE) + return {}; + + SourceLocation SourceLocationBeg = getBeginningOfIdentifier(AST, Pos, FE); + + auto DeclMacrosFinder = std::make_shared<DeclarationAndMacrosFinder>( llvm::errs(), SourceLocationBeg, AST.getASTContext(), AST.getPreprocessor()); index::IndexingOptions IndexOpts; IndexOpts.SystemSymbolFilter = index::IndexingOptions::SystemSymbolFilterKind::All; IndexOpts.IndexFunctionLocals = true; + // Macro occurences are not currently handled. + indexTopLevelDecls(AST.getASTContext(), AST.getTopLevelDecls(), + DeclMacrosFinder, IndexOpts); + + std::vector<const Decl *> SelectedDecls = DeclMacrosFinder->takeDecls(); + + auto DocHighlightsFinder = std::make_shared<DocumentHighlightsFinder>( + llvm::errs(), AST.getASTContext(), AST.getPreprocessor(), SelectedDecls); + indexTopLevelDecls(AST.getASTContext(), AST.getTopLevelDecls(), - DeclLocationsFinder, IndexOpts); + DocHighlightsFinder, IndexOpts); - return DeclLocationsFinder->takeLocations(); + return DocHighlightsFinder->takeHighlights(); } void ParsedAST::ensurePreambleDeclsDeserialized() { Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -286,6 +286,10 @@ /// given a header file and vice versa. llvm::Optional<Path> switchSourceHeader(PathRef Path); + /// Get document highlights for a given position. + llvm::Expected<Tagged<std::vector<DocumentHighlight>>> + findDocumentHighlights(PathRef File, Position Pos); + /// Run formatting for \p Rng inside \p File. std::vector<tooling::Replacement> formatRange(PathRef File, Range Rng); /// Run formatting for the whole \p File. Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -509,6 +509,39 @@ return llvm::None; } +llvm::Expected<Tagged<std::vector<DocumentHighlight>>> +ClangdServer::findDocumentHighlights(PathRef File, Position Pos) { + auto FileContents = DraftMgr.getDraft(File); + if (!FileContents.Draft) + return llvm::make_error<llvm::StringError>( + "findDocumentHighlights called on non-added file", + llvm::errc::invalid_argument); + + auto TaggedFS = FSProvider.getTaggedFileSystem(File); + + std::shared_ptr<CppFile> Resources = Units.getFile(File); + if (!Resources) + return llvm::make_error<llvm::StringError>( + "findDocumentHighlights called on non-added file", + llvm::errc::invalid_argument); + + std::vector<DocumentHighlight> Result; + llvm::Optional<llvm::Error> Err; + Resources->getAST().get()->runUnderLock([Pos, &Result, &Err, + this](ParsedAST *AST) { + if (!AST) { + Err = llvm::make_error<llvm::StringError>("Invalid AST", + llvm::errc::invalid_argument); + return; + } + Result = clangd::findDocumentHighlights(*AST, Pos, Logger); + }); + + if (Err) + return std::move(*Err); + return make_tagged(Result, TaggedFS.Tag); +} + std::future<void> ClangdServer::scheduleReparseAndDiags( PathRef File, VersionedDraft Contents, std::shared_ptr<CppFile> Resources, Tagged<IntrusiveRefCntPtr<vfs::FileSystem>> TaggedFS) { Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -69,6 +69,7 @@ void onSignatureHelp(Ctx C, TextDocumentPositionParams &Params) override; void onGoToDefinition(Ctx C, TextDocumentPositionParams &Params) override; void onSwitchSourceHeader(Ctx C, TextDocumentIdentifier &Params) override; + void onDocumentHighlight(Ctx C, TextDocumentPositionParams &Params) override; void onFileEvent(Ctx C, DidChangeWatchedFilesParams &Params) override; void onCommand(Ctx C, ExecuteCommandParams &Params) override; void onRename(Ctx C, RenameParams &Parames) override; Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -57,6 +57,7 @@ {"triggerCharacters", {"(", ","}}, }}, {"definitionProvider", true}, + {"documentHighlightProvider", true}, {"renameProvider", true}, {"executeCommandProvider", json::obj{ @@ -235,6 +236,22 @@ C.reply(Result ? URI::fromFile(*Result).uri : ""); } +void ClangdLSPServer::onDocumentHighlight(Ctx C, + TextDocumentPositionParams &Params) { + + auto Highlights = Server.findDocumentHighlights( + Params.textDocument.uri.file, + Position{Params.position.line, Params.position.character}); + + if (!Highlights) { + C.replyError(ErrorCode::InternalError, + llvm::toString(Highlights.takeError())); + return; + } + + C.reply(json::ary(Highlights->Value)); +} + ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, unsigned AsyncThreadsCount, bool StorePreamblesInMemory, const clangd::CodeCompleteOptions &CCOpts,
_______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org http://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits